@khanacademy/wonder-blocks-form 4.9.2 → 4.9.4
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/CHANGELOG.md +21 -0
- package/dist/components/checkbox-core.d.ts +2 -2
- package/dist/components/checkbox.d.ts +2 -2
- package/dist/components/choice-internal.d.ts +2 -2
- package/dist/components/choice.d.ts +2 -2
- package/dist/components/labeled-text-field.d.ts +9 -1
- package/dist/components/radio-core.d.ts +2 -2
- package/dist/components/radio.d.ts +2 -2
- package/dist/components/text-area.d.ts +2 -2
- package/dist/components/text-field.d.ts +10 -5
- package/dist/es/index.js +93 -48
- package/dist/index.js +93 -48
- package/package.json +7 -7
- package/src/__tests__/__snapshots__/custom-snapshot.test.tsx.snap +0 -247
- package/src/__tests__/custom-snapshot.test.tsx +0 -48
- package/src/components/__tests__/checkbox-group.test.tsx +0 -162
- package/src/components/__tests__/checkbox.test.tsx +0 -138
- package/src/components/__tests__/field-heading.test.tsx +0 -225
- package/src/components/__tests__/labeled-text-field.test.tsx +0 -750
- package/src/components/__tests__/radio-group.test.tsx +0 -182
- package/src/components/__tests__/text-area.test.tsx +0 -1286
- package/src/components/__tests__/text-field.test.tsx +0 -562
- package/src/components/checkbox-core.tsx +0 -239
- package/src/components/checkbox-group.tsx +0 -174
- package/src/components/checkbox.tsx +0 -99
- package/src/components/choice-internal.tsx +0 -184
- package/src/components/choice.tsx +0 -157
- package/src/components/field-heading.tsx +0 -169
- package/src/components/group-styles.ts +0 -33
- package/src/components/labeled-text-field.tsx +0 -317
- package/src/components/radio-core.tsx +0 -171
- package/src/components/radio-group.tsx +0 -159
- package/src/components/radio.tsx +0 -82
- package/src/components/text-area.tsx +0 -430
- package/src/components/text-field.tsx +0 -399
- package/src/index.ts +0 -17
- package/src/util/types.ts +0 -85
- package/tsconfig-build.json +0 -19
- package/tsconfig-build.tsbuildinfo +0 -1
|
@@ -1,239 +0,0 @@
|
|
|
1
|
-
import * as React from "react";
|
|
2
|
-
import {StyleSheet} from "aphrodite";
|
|
3
|
-
|
|
4
|
-
import {mix, color, spacing} from "@khanacademy/wonder-blocks-tokens";
|
|
5
|
-
import {addStyle} from "@khanacademy/wonder-blocks-core";
|
|
6
|
-
import {PhosphorIcon} from "@khanacademy/wonder-blocks-icon";
|
|
7
|
-
import checkIcon from "@phosphor-icons/core/bold/check-bold.svg";
|
|
8
|
-
import minusIcon from "@phosphor-icons/core/bold/minus-bold.svg";
|
|
9
|
-
|
|
10
|
-
import type {ChoiceCoreProps, Checked} from "../util/types";
|
|
11
|
-
|
|
12
|
-
// `AriaChecked` and `mapCheckedToAriaChecked()` are used to convert the
|
|
13
|
-
// `checked` prop value to a value that a screen reader can understand via the
|
|
14
|
-
// `aria-checked` attribute
|
|
15
|
-
type AriaChecked = "true" | "false" | "mixed";
|
|
16
|
-
|
|
17
|
-
function mapCheckedToAriaChecked(value: Checked): AriaChecked {
|
|
18
|
-
switch (value) {
|
|
19
|
-
case true:
|
|
20
|
-
return "true";
|
|
21
|
-
case false:
|
|
22
|
-
return "false";
|
|
23
|
-
default:
|
|
24
|
-
return "mixed";
|
|
25
|
-
}
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
const {blue, red, white, offWhite, offBlack16, offBlack32, offBlack50} = color;
|
|
29
|
-
|
|
30
|
-
// The checkbox size
|
|
31
|
-
const size = spacing.medium_16;
|
|
32
|
-
// The check icon size
|
|
33
|
-
const checkSize = spacing.small_12;
|
|
34
|
-
|
|
35
|
-
const StyledInput = addStyle("input");
|
|
36
|
-
|
|
37
|
-
/**
|
|
38
|
-
* The internal stateless ☑️ Checkbox
|
|
39
|
-
*/
|
|
40
|
-
const CheckboxCore = React.forwardRef(function CheckboxCore(
|
|
41
|
-
props: ChoiceCoreProps,
|
|
42
|
-
ref: React.ForwardedRef<HTMLInputElement>,
|
|
43
|
-
) {
|
|
44
|
-
const {checked, disabled, error, groupName, id, testId, ...sharedProps} =
|
|
45
|
-
props;
|
|
46
|
-
|
|
47
|
-
const innerRef = React.useRef<HTMLInputElement>(null);
|
|
48
|
-
|
|
49
|
-
React.useEffect(() => {
|
|
50
|
-
// Keep the indeterminate state in sync with the checked prop
|
|
51
|
-
if (innerRef.current != null) {
|
|
52
|
-
innerRef.current.indeterminate = checked == null;
|
|
53
|
-
}
|
|
54
|
-
}, [checked, innerRef]);
|
|
55
|
-
|
|
56
|
-
const handleChange: () => void = () => {
|
|
57
|
-
// Empty because change is handled by ClickableBehavior
|
|
58
|
-
return;
|
|
59
|
-
};
|
|
60
|
-
|
|
61
|
-
const stateStyles = _generateStyles(checked, error);
|
|
62
|
-
|
|
63
|
-
const defaultStyle = [
|
|
64
|
-
sharedStyles.inputReset,
|
|
65
|
-
sharedStyles.default,
|
|
66
|
-
!disabled && stateStyles.default,
|
|
67
|
-
disabled && sharedStyles.disabled,
|
|
68
|
-
];
|
|
69
|
-
|
|
70
|
-
const checkboxIcon = (
|
|
71
|
-
<PhosphorIcon
|
|
72
|
-
color={disabled ? offBlack32 : white}
|
|
73
|
-
icon={checked ? checkIcon : minusIcon}
|
|
74
|
-
size="small"
|
|
75
|
-
style={[
|
|
76
|
-
sharedStyles.checkboxIcon,
|
|
77
|
-
// The check icon is smaller than the checkbox, as per design.
|
|
78
|
-
{
|
|
79
|
-
width: checkSize,
|
|
80
|
-
height: checkSize,
|
|
81
|
-
},
|
|
82
|
-
]}
|
|
83
|
-
/>
|
|
84
|
-
);
|
|
85
|
-
|
|
86
|
-
const ariaChecked = mapCheckedToAriaChecked(checked);
|
|
87
|
-
|
|
88
|
-
return (
|
|
89
|
-
<React.Fragment>
|
|
90
|
-
<StyledInput
|
|
91
|
-
{...sharedProps}
|
|
92
|
-
ref={(node) => {
|
|
93
|
-
// @ts-expect-error: current is not actually read-only
|
|
94
|
-
innerRef.current = node;
|
|
95
|
-
if (typeof ref === "function") {
|
|
96
|
-
ref(node);
|
|
97
|
-
} else if (ref != null) {
|
|
98
|
-
ref.current = node;
|
|
99
|
-
}
|
|
100
|
-
}}
|
|
101
|
-
type="checkbox"
|
|
102
|
-
aria-checked={ariaChecked}
|
|
103
|
-
aria-invalid={error}
|
|
104
|
-
checked={checked ?? undefined}
|
|
105
|
-
disabled={disabled}
|
|
106
|
-
id={id}
|
|
107
|
-
name={groupName}
|
|
108
|
-
// Need to specify because this is a controlled React form
|
|
109
|
-
// component, but we handle the click via ClickableBehavior
|
|
110
|
-
onChange={handleChange}
|
|
111
|
-
style={defaultStyle}
|
|
112
|
-
data-testid={testId}
|
|
113
|
-
/>
|
|
114
|
-
{checked || checked == null ? checkboxIcon : <></>}
|
|
115
|
-
</React.Fragment>
|
|
116
|
-
);
|
|
117
|
-
});
|
|
118
|
-
|
|
119
|
-
const sharedStyles = StyleSheet.create({
|
|
120
|
-
// Reset the default styled input element
|
|
121
|
-
inputReset: {
|
|
122
|
-
appearance: "none",
|
|
123
|
-
WebkitAppearance: "none",
|
|
124
|
-
MozAppearance: "none",
|
|
125
|
-
},
|
|
126
|
-
|
|
127
|
-
default: {
|
|
128
|
-
height: size,
|
|
129
|
-
width: size,
|
|
130
|
-
minHeight: size,
|
|
131
|
-
minWidth: size,
|
|
132
|
-
margin: 0,
|
|
133
|
-
outline: "none",
|
|
134
|
-
boxSizing: "border-box",
|
|
135
|
-
borderStyle: "solid",
|
|
136
|
-
borderWidth: 1,
|
|
137
|
-
borderRadius: 3,
|
|
138
|
-
},
|
|
139
|
-
|
|
140
|
-
disabled: {
|
|
141
|
-
cursor: "auto",
|
|
142
|
-
backgroundColor: offWhite,
|
|
143
|
-
borderColor: offBlack16,
|
|
144
|
-
borderWidth: 1,
|
|
145
|
-
},
|
|
146
|
-
|
|
147
|
-
checkboxIcon: {
|
|
148
|
-
position: "absolute",
|
|
149
|
-
pointerEvents: "none",
|
|
150
|
-
// This margin is to center the check icon in the checkbox.
|
|
151
|
-
margin: (size - checkSize) / 2,
|
|
152
|
-
},
|
|
153
|
-
});
|
|
154
|
-
|
|
155
|
-
const fadedBlue = mix(color.fadedBlue16, white);
|
|
156
|
-
const activeBlue = color.activeBlue;
|
|
157
|
-
const fadedRed = mix(color.fadedRed8, white);
|
|
158
|
-
const activeRed = color.activeRed;
|
|
159
|
-
|
|
160
|
-
const colors = {
|
|
161
|
-
default: {
|
|
162
|
-
faded: fadedBlue,
|
|
163
|
-
base: blue,
|
|
164
|
-
active: activeBlue,
|
|
165
|
-
},
|
|
166
|
-
error: {
|
|
167
|
-
faded: fadedRed,
|
|
168
|
-
base: red,
|
|
169
|
-
active: activeRed,
|
|
170
|
-
},
|
|
171
|
-
} as const;
|
|
172
|
-
|
|
173
|
-
const styles: Record<string, any> = {};
|
|
174
|
-
|
|
175
|
-
const _generateStyles = (checked: Checked, error: boolean) => {
|
|
176
|
-
// "hash" the parameters
|
|
177
|
-
const styleKey = `${String(checked)}-${String(error)}`;
|
|
178
|
-
if (styles[styleKey]) {
|
|
179
|
-
return styles[styleKey];
|
|
180
|
-
}
|
|
181
|
-
|
|
182
|
-
const palette = error ? colors.error : colors.default;
|
|
183
|
-
|
|
184
|
-
let newStyles: Record<string, any> = {};
|
|
185
|
-
if (checked || checked == null) {
|
|
186
|
-
newStyles = {
|
|
187
|
-
default: {
|
|
188
|
-
backgroundColor: palette.base,
|
|
189
|
-
borderWidth: 0,
|
|
190
|
-
|
|
191
|
-
// Focus and hover have the same style. Focus style only shows
|
|
192
|
-
// up with keyboard navigation.
|
|
193
|
-
":focus-visible": {
|
|
194
|
-
boxShadow: `0 0 0 1px ${white}, 0 0 0 3px ${palette.base}`,
|
|
195
|
-
},
|
|
196
|
-
|
|
197
|
-
":hover": {
|
|
198
|
-
boxShadow: `0 0 0 1px ${white}, 0 0 0 3px ${palette.base}`,
|
|
199
|
-
},
|
|
200
|
-
|
|
201
|
-
":active": {
|
|
202
|
-
boxShadow: `0 0 0 1px ${white}, 0 0 0 3px ${palette.active}`,
|
|
203
|
-
background: palette.active,
|
|
204
|
-
},
|
|
205
|
-
},
|
|
206
|
-
};
|
|
207
|
-
} else {
|
|
208
|
-
newStyles = {
|
|
209
|
-
default: {
|
|
210
|
-
backgroundColor: error ? fadedRed : white,
|
|
211
|
-
borderColor: error ? red : offBlack50,
|
|
212
|
-
|
|
213
|
-
// Focus and hover have the same style. Focus style only shows
|
|
214
|
-
// up with keyboard navigation.
|
|
215
|
-
":focus-visible": {
|
|
216
|
-
backgroundColor: error ? fadedRed : white,
|
|
217
|
-
borderColor: palette.base,
|
|
218
|
-
borderWidth: 2,
|
|
219
|
-
},
|
|
220
|
-
|
|
221
|
-
":hover": {
|
|
222
|
-
backgroundColor: error ? fadedRed : white,
|
|
223
|
-
borderColor: palette.base,
|
|
224
|
-
borderWidth: 2,
|
|
225
|
-
},
|
|
226
|
-
|
|
227
|
-
":active": {
|
|
228
|
-
backgroundColor: palette.faded,
|
|
229
|
-
borderColor: error ? activeRed : blue,
|
|
230
|
-
borderWidth: 2,
|
|
231
|
-
},
|
|
232
|
-
},
|
|
233
|
-
};
|
|
234
|
-
}
|
|
235
|
-
styles[styleKey] = StyleSheet.create(newStyles);
|
|
236
|
-
return styles[styleKey];
|
|
237
|
-
};
|
|
238
|
-
|
|
239
|
-
export default CheckboxCore;
|
|
@@ -1,174 +0,0 @@
|
|
|
1
|
-
import * as React from "react";
|
|
2
|
-
|
|
3
|
-
import {View, addStyle} from "@khanacademy/wonder-blocks-core";
|
|
4
|
-
import {Strut} from "@khanacademy/wonder-blocks-layout";
|
|
5
|
-
import {spacing} from "@khanacademy/wonder-blocks-tokens";
|
|
6
|
-
import {LabelMedium, LabelSmall} from "@khanacademy/wonder-blocks-typography";
|
|
7
|
-
import type {StyleType} from "@khanacademy/wonder-blocks-core";
|
|
8
|
-
|
|
9
|
-
import styles from "./group-styles";
|
|
10
|
-
import Choice from "./choice";
|
|
11
|
-
|
|
12
|
-
// Keep synced with CheckboxGroupProps in ../util/types.js
|
|
13
|
-
type CheckboxGroupProps = {
|
|
14
|
-
/**
|
|
15
|
-
* Children should be Choice components.
|
|
16
|
-
*/
|
|
17
|
-
children: Array<
|
|
18
|
-
| React.ReactElement<React.ComponentProps<typeof Choice>>
|
|
19
|
-
| false
|
|
20
|
-
| null
|
|
21
|
-
| undefined
|
|
22
|
-
>;
|
|
23
|
-
/**
|
|
24
|
-
* Group name for this checkbox or radio group. Should be unique for all
|
|
25
|
-
* such groups displayed on a page.
|
|
26
|
-
*/
|
|
27
|
-
groupName: string;
|
|
28
|
-
/**
|
|
29
|
-
* Optional label for the group. This label is optional to allow for
|
|
30
|
-
* greater flexibility in implementing checkbox and radio groups.
|
|
31
|
-
*/
|
|
32
|
-
label?: React.ReactNode;
|
|
33
|
-
/**
|
|
34
|
-
* Optional description for the group.
|
|
35
|
-
*/
|
|
36
|
-
description?: React.ReactNode;
|
|
37
|
-
/**
|
|
38
|
-
* Optional error message. If supplied, the group will be displayed in an
|
|
39
|
-
* error state, along with this error message. If no error state is desired,
|
|
40
|
-
* simply do not supply this prop, or pass along null.
|
|
41
|
-
*/
|
|
42
|
-
errorMessage?: string | null | undefined;
|
|
43
|
-
/**
|
|
44
|
-
* Custom styling for this group of checkboxes.
|
|
45
|
-
*/
|
|
46
|
-
style?: StyleType;
|
|
47
|
-
/**
|
|
48
|
-
* Callback for when selection of the group has changed. Passes the newly
|
|
49
|
-
* selected values.
|
|
50
|
-
*/
|
|
51
|
-
onChange: (selectedValues: Array<string>) => unknown;
|
|
52
|
-
/**
|
|
53
|
-
* An array of the values of the selected values in this checkbox group.
|
|
54
|
-
*/
|
|
55
|
-
selectedValues: Array<string>;
|
|
56
|
-
/**
|
|
57
|
-
* Test ID used for e2e testing.
|
|
58
|
-
*/
|
|
59
|
-
testId?: string;
|
|
60
|
-
};
|
|
61
|
-
|
|
62
|
-
const StyledFieldset = addStyle("fieldset");
|
|
63
|
-
const StyledLegend = addStyle("legend");
|
|
64
|
-
|
|
65
|
-
/**
|
|
66
|
-
* A checkbox group allows multiple selection. This component auto-populates
|
|
67
|
-
* many props for its children Choice components. The Choice component is
|
|
68
|
-
* exposed for the user to apply custom styles or to indicate which choices are
|
|
69
|
-
* disabled.
|
|
70
|
-
*
|
|
71
|
-
* ### Usage
|
|
72
|
-
*
|
|
73
|
-
* ```jsx
|
|
74
|
-
* import {Choice, CheckboxGroup} from "@khanacademy/wonder-blocks-form";
|
|
75
|
-
*
|
|
76
|
-
* const [selectedValues, setSelectedValues] = React.useState([]);
|
|
77
|
-
*
|
|
78
|
-
* <CheckboxGroup
|
|
79
|
-
* label="some-label"
|
|
80
|
-
* description="some-description"
|
|
81
|
-
* groupName="some-group-name"
|
|
82
|
-
* onChange={setSelectedValues}
|
|
83
|
-
* selectedValues={selectedValues}
|
|
84
|
-
* >
|
|
85
|
-
* // Add as many choices as necessary
|
|
86
|
-
* <Choice
|
|
87
|
-
* label="Choice 1"
|
|
88
|
-
* value="some-choice-value"
|
|
89
|
-
* />
|
|
90
|
-
* <Choice
|
|
91
|
-
* label="Choice 2"
|
|
92
|
-
* value="some-choice-value-2"
|
|
93
|
-
* description="Some choice description."
|
|
94
|
-
* />
|
|
95
|
-
* </CheckboxGroup>
|
|
96
|
-
* ```
|
|
97
|
-
*/
|
|
98
|
-
const CheckboxGroup = React.forwardRef(function CheckboxGroup(
|
|
99
|
-
props: CheckboxGroupProps,
|
|
100
|
-
ref: React.ForwardedRef<HTMLFieldSetElement>,
|
|
101
|
-
) {
|
|
102
|
-
const {
|
|
103
|
-
children,
|
|
104
|
-
label,
|
|
105
|
-
description,
|
|
106
|
-
errorMessage,
|
|
107
|
-
groupName,
|
|
108
|
-
onChange,
|
|
109
|
-
selectedValues,
|
|
110
|
-
style,
|
|
111
|
-
testId,
|
|
112
|
-
} = props;
|
|
113
|
-
|
|
114
|
-
const handleChange = (
|
|
115
|
-
changedValue: string,
|
|
116
|
-
originalCheckedState: boolean,
|
|
117
|
-
) => {
|
|
118
|
-
if (originalCheckedState) {
|
|
119
|
-
const index = selectedValues.indexOf(changedValue);
|
|
120
|
-
const updatedSelection = [
|
|
121
|
-
...selectedValues.slice(0, index),
|
|
122
|
-
...selectedValues.slice(index + 1),
|
|
123
|
-
];
|
|
124
|
-
onChange(updatedSelection);
|
|
125
|
-
} else {
|
|
126
|
-
onChange([...selectedValues, changedValue]);
|
|
127
|
-
}
|
|
128
|
-
};
|
|
129
|
-
|
|
130
|
-
const allChildren = React.Children.toArray(children).filter(Boolean);
|
|
131
|
-
|
|
132
|
-
return (
|
|
133
|
-
<StyledFieldset data-testid={testId} style={styles.fieldset} ref={ref}>
|
|
134
|
-
{/* We have a View here because fieldset cannot be used with flexbox*/}
|
|
135
|
-
<View style={style}>
|
|
136
|
-
{label && (
|
|
137
|
-
<StyledLegend style={styles.legend}>
|
|
138
|
-
<LabelMedium>{label}</LabelMedium>
|
|
139
|
-
</StyledLegend>
|
|
140
|
-
)}
|
|
141
|
-
{description && (
|
|
142
|
-
<LabelSmall style={styles.description}>
|
|
143
|
-
{description}
|
|
144
|
-
</LabelSmall>
|
|
145
|
-
)}
|
|
146
|
-
{errorMessage && (
|
|
147
|
-
<LabelSmall style={styles.error}>{errorMessage}</LabelSmall>
|
|
148
|
-
)}
|
|
149
|
-
{(label || description || errorMessage) && (
|
|
150
|
-
<Strut size={spacing.small_12} />
|
|
151
|
-
)}
|
|
152
|
-
|
|
153
|
-
{allChildren.map((child, index) => {
|
|
154
|
-
// @ts-expect-error [FEI-5019] - TS2339 - Property 'props' does not exist on type 'ReactChild | ReactFragment | ReactPortal'.
|
|
155
|
-
const {style, value} = child.props;
|
|
156
|
-
const checked = selectedValues.includes(value);
|
|
157
|
-
// @ts-expect-error [FEI-5019] - TS2769 - No overload matches this call.
|
|
158
|
-
return React.cloneElement(child, {
|
|
159
|
-
checked: checked,
|
|
160
|
-
error: !!errorMessage,
|
|
161
|
-
groupName: groupName,
|
|
162
|
-
id: `${groupName}-${value}`,
|
|
163
|
-
key: value,
|
|
164
|
-
onChange: () => handleChange(value, checked),
|
|
165
|
-
style: [index > 0 && styles.defaultLineGap, style],
|
|
166
|
-
variant: "checkbox",
|
|
167
|
-
});
|
|
168
|
-
})}
|
|
169
|
-
</View>
|
|
170
|
-
</StyledFieldset>
|
|
171
|
-
);
|
|
172
|
-
});
|
|
173
|
-
|
|
174
|
-
export default CheckboxGroup;
|
|
@@ -1,99 +0,0 @@
|
|
|
1
|
-
import * as React from "react";
|
|
2
|
-
|
|
3
|
-
import type {AriaProps, StyleType} from "@khanacademy/wonder-blocks-core";
|
|
4
|
-
import type {Checked} from "../util/types";
|
|
5
|
-
|
|
6
|
-
import ChoiceInternal from "./choice-internal";
|
|
7
|
-
|
|
8
|
-
// Keep synced with ChoiceComponentProps in ../util/types.js
|
|
9
|
-
type ChoiceComponentProps = AriaProps & {
|
|
10
|
-
/**
|
|
11
|
-
* Whether this component is checked or indeterminate
|
|
12
|
-
*/
|
|
13
|
-
checked: Checked;
|
|
14
|
-
/**
|
|
15
|
-
* Whether this component is disabled
|
|
16
|
-
*/
|
|
17
|
-
disabled?: boolean;
|
|
18
|
-
/**
|
|
19
|
-
* Whether this component should show an error state
|
|
20
|
-
*/
|
|
21
|
-
error?: boolean;
|
|
22
|
-
/**
|
|
23
|
-
* Callback when this component is selected. The newCheckedState is the
|
|
24
|
-
* new checked state of the component.
|
|
25
|
-
*/
|
|
26
|
-
onChange: (newCheckedState: boolean) => unknown;
|
|
27
|
-
/**
|
|
28
|
-
* Optional label for the field.
|
|
29
|
-
*/
|
|
30
|
-
label?: React.ReactNode;
|
|
31
|
-
/**
|
|
32
|
-
* Optional description for the field.
|
|
33
|
-
*/
|
|
34
|
-
description?: React.ReactNode;
|
|
35
|
-
/**
|
|
36
|
-
* Unique identifier attached to the HTML input element. If used, need to
|
|
37
|
-
* guarantee that the ID is unique within everything rendered on a page.
|
|
38
|
-
* Used to match `<label>` with `<input>` elements for screenreaders.
|
|
39
|
-
*/
|
|
40
|
-
id?: string;
|
|
41
|
-
/**
|
|
42
|
-
* Optional styling for the container. Does not style the component.
|
|
43
|
-
*/
|
|
44
|
-
style?: StyleType;
|
|
45
|
-
/**
|
|
46
|
-
* Adds CSS classes to the Checkbox.
|
|
47
|
-
*/
|
|
48
|
-
className?: string;
|
|
49
|
-
/**
|
|
50
|
-
* Optional test ID for e2e testing
|
|
51
|
-
*/
|
|
52
|
-
testId?: string;
|
|
53
|
-
/**
|
|
54
|
-
* Name for the checkbox or radio button group. Only applicable for group
|
|
55
|
-
* contexts, auto-populated by group components via Choice.
|
|
56
|
-
* @ignore
|
|
57
|
-
*/
|
|
58
|
-
groupName?: string;
|
|
59
|
-
};
|
|
60
|
-
|
|
61
|
-
/**
|
|
62
|
-
* ☑️ A nicely styled checkbox for all your checking needs. Can optionally take
|
|
63
|
-
* label and description props.
|
|
64
|
-
*
|
|
65
|
-
* If used by itself, a checkbox provides two options - checked and unchecked.
|
|
66
|
-
* A group of checkboxes can be used to allow a user to select multiple values
|
|
67
|
-
* from a list of options.
|
|
68
|
-
*
|
|
69
|
-
* If you want a whole group of Checkbox[es] that are related, see the Choice
|
|
70
|
-
* and CheckboxGroup components.
|
|
71
|
-
*
|
|
72
|
-
* ### Usage
|
|
73
|
-
*
|
|
74
|
-
* ```jsx
|
|
75
|
-
* import {Checkbox} from "@khanacademy/wonder-blocks-form";
|
|
76
|
-
*
|
|
77
|
-
* const [checked, setChecked] = React.useState(false);
|
|
78
|
-
*
|
|
79
|
-
* <Checkbox checked={checked} onChange={setChecked} />
|
|
80
|
-
* ```
|
|
81
|
-
*/
|
|
82
|
-
const Checkbox = React.forwardRef(function Checkbox(
|
|
83
|
-
props: ChoiceComponentProps,
|
|
84
|
-
ref: React.ForwardedRef<HTMLInputElement>,
|
|
85
|
-
) {
|
|
86
|
-
const {disabled = false, error = false} = props;
|
|
87
|
-
|
|
88
|
-
return (
|
|
89
|
-
<ChoiceInternal
|
|
90
|
-
{...props}
|
|
91
|
-
variant="checkbox"
|
|
92
|
-
disabled={disabled}
|
|
93
|
-
error={error}
|
|
94
|
-
ref={ref}
|
|
95
|
-
/>
|
|
96
|
-
);
|
|
97
|
-
});
|
|
98
|
-
|
|
99
|
-
export default Checkbox;
|
|
@@ -1,184 +0,0 @@
|
|
|
1
|
-
import * as React from "react";
|
|
2
|
-
import {StyleSheet} from "aphrodite";
|
|
3
|
-
|
|
4
|
-
import {View, UniqueIDProvider} from "@khanacademy/wonder-blocks-core";
|
|
5
|
-
import {Strut} from "@khanacademy/wonder-blocks-layout";
|
|
6
|
-
import {color, spacing} from "@khanacademy/wonder-blocks-tokens";
|
|
7
|
-
import {LabelMedium, LabelSmall} from "@khanacademy/wonder-blocks-typography";
|
|
8
|
-
import type {AriaProps, StyleType} from "@khanacademy/wonder-blocks-core";
|
|
9
|
-
import CheckboxCore from "./checkbox-core";
|
|
10
|
-
import RadioCore from "./radio-core";
|
|
11
|
-
|
|
12
|
-
type Props = AriaProps & {
|
|
13
|
-
/** Whether this choice is checked. */
|
|
14
|
-
checked: boolean | null | undefined;
|
|
15
|
-
/** Whether this choice option is disabled. */
|
|
16
|
-
disabled?: boolean;
|
|
17
|
-
/** Whether this choice is in error mode. */
|
|
18
|
-
error?: boolean;
|
|
19
|
-
/** Returns the new checked state of the component. */
|
|
20
|
-
onChange: (newCheckedState: boolean) => unknown;
|
|
21
|
-
/**
|
|
22
|
-
* Used for accessibility purposes, where the label id should match the
|
|
23
|
-
* input id.
|
|
24
|
-
*/
|
|
25
|
-
id?: string;
|
|
26
|
-
/**
|
|
27
|
-
* Optional additional styling.
|
|
28
|
-
*/
|
|
29
|
-
style?: StyleType;
|
|
30
|
-
/**
|
|
31
|
-
* Adds CSS classes to the Button.
|
|
32
|
-
*/
|
|
33
|
-
className?: string;
|
|
34
|
-
/**
|
|
35
|
-
* Optional id for testing purposes.
|
|
36
|
-
*/
|
|
37
|
-
testId?: string;
|
|
38
|
-
/**
|
|
39
|
-
* Label for the field.
|
|
40
|
-
*/
|
|
41
|
-
label?: React.ReactNode;
|
|
42
|
-
/** Optional description for the field. */
|
|
43
|
-
description?: React.ReactNode;
|
|
44
|
-
/** Auto-populated by parent's groupName prop if in a group. */
|
|
45
|
-
groupName?: string;
|
|
46
|
-
/** Takes either "radio" or "checkbox" value. */
|
|
47
|
-
variant: "radio" | "checkbox";
|
|
48
|
-
};
|
|
49
|
-
|
|
50
|
-
/**
|
|
51
|
-
* This is a potentially labeled 🔘 or ☑️ item. This is an internal component
|
|
52
|
-
* that's wrapped by Checkbox and Radio. Choice is a wrapper for Checkbox and
|
|
53
|
-
* Radio with many of its props auto-populated, to be used with CheckboxGroup
|
|
54
|
-
* and RadioGroup. This design allows for more explicit prop typing. For
|
|
55
|
-
* example, we can make onChange a required prop on Checkbox but not on Choice
|
|
56
|
-
* (because for Choice, that prop would be auto-populated by CheckboxGroup).
|
|
57
|
-
*/ const ChoiceInternal = React.forwardRef(function ChoiceInternal(
|
|
58
|
-
props: Props,
|
|
59
|
-
ref: React.ForwardedRef<HTMLInputElement>,
|
|
60
|
-
) {
|
|
61
|
-
const {
|
|
62
|
-
checked,
|
|
63
|
-
description,
|
|
64
|
-
disabled = false,
|
|
65
|
-
error = false,
|
|
66
|
-
id,
|
|
67
|
-
label,
|
|
68
|
-
onChange,
|
|
69
|
-
style,
|
|
70
|
-
className,
|
|
71
|
-
variant,
|
|
72
|
-
...coreProps
|
|
73
|
-
} = props;
|
|
74
|
-
|
|
75
|
-
const handleClick: () => void = () => {
|
|
76
|
-
// Radio buttons cannot be unchecked
|
|
77
|
-
if (variant === "radio" && checked) {
|
|
78
|
-
return;
|
|
79
|
-
}
|
|
80
|
-
onChange(!checked);
|
|
81
|
-
};
|
|
82
|
-
|
|
83
|
-
const getChoiceCoreComponent = ():
|
|
84
|
-
| typeof RadioCore
|
|
85
|
-
| typeof CheckboxCore => {
|
|
86
|
-
if (variant === "radio") {
|
|
87
|
-
return RadioCore;
|
|
88
|
-
} else {
|
|
89
|
-
return CheckboxCore;
|
|
90
|
-
}
|
|
91
|
-
};
|
|
92
|
-
|
|
93
|
-
const getLabel = (id: string): React.ReactNode => {
|
|
94
|
-
return (
|
|
95
|
-
<LabelMedium
|
|
96
|
-
style={[styles.label, disabled && styles.disabledLabel]}
|
|
97
|
-
>
|
|
98
|
-
<label htmlFor={id}>{label}</label>
|
|
99
|
-
</LabelMedium>
|
|
100
|
-
);
|
|
101
|
-
};
|
|
102
|
-
|
|
103
|
-
const getDescription = (id?: string): React.ReactNode => {
|
|
104
|
-
return (
|
|
105
|
-
<LabelSmall style={styles.description} id={id}>
|
|
106
|
-
{description}
|
|
107
|
-
</LabelSmall>
|
|
108
|
-
);
|
|
109
|
-
};
|
|
110
|
-
|
|
111
|
-
const ChoiceCore = getChoiceCoreComponent();
|
|
112
|
-
|
|
113
|
-
return (
|
|
114
|
-
<UniqueIDProvider mockOnFirstRender={true} scope="choice">
|
|
115
|
-
{(ids) => {
|
|
116
|
-
// A choice element should always have a unique ID set
|
|
117
|
-
// so that the label can always refer to this element.
|
|
118
|
-
// This guarantees that clicking on the label will
|
|
119
|
-
// always click on the choice as well. If an ID is
|
|
120
|
-
// passed in as a prop, use that one. Otherwise,
|
|
121
|
-
// create a unique ID using the provider.
|
|
122
|
-
const uniqueId = id || ids.get("main");
|
|
123
|
-
|
|
124
|
-
// Create a unique ID for the description section to be
|
|
125
|
-
// used by this element's `aria-describedby`.
|
|
126
|
-
const descriptionId = description
|
|
127
|
-
? ids.get("description")
|
|
128
|
-
: undefined;
|
|
129
|
-
|
|
130
|
-
return (
|
|
131
|
-
<View style={style} className={className}>
|
|
132
|
-
<View
|
|
133
|
-
style={styles.wrapper}
|
|
134
|
-
// We are resetting the tabIndex=0 from handlers
|
|
135
|
-
// because the ChoiceCore component will receive
|
|
136
|
-
// focus on basis of it being an input element.
|
|
137
|
-
tabIndex={-1}
|
|
138
|
-
>
|
|
139
|
-
<ChoiceCore
|
|
140
|
-
{...coreProps}
|
|
141
|
-
id={uniqueId}
|
|
142
|
-
checked={checked}
|
|
143
|
-
aria-describedby={descriptionId}
|
|
144
|
-
onClick={handleClick}
|
|
145
|
-
disabled={disabled}
|
|
146
|
-
error={error}
|
|
147
|
-
ref={ref}
|
|
148
|
-
/>
|
|
149
|
-
<Strut size={spacing.xSmall_8} />
|
|
150
|
-
{label && getLabel(uniqueId)}
|
|
151
|
-
</View>
|
|
152
|
-
{description && getDescription(descriptionId)}
|
|
153
|
-
</View>
|
|
154
|
-
);
|
|
155
|
-
}}
|
|
156
|
-
</UniqueIDProvider>
|
|
157
|
-
);
|
|
158
|
-
});
|
|
159
|
-
|
|
160
|
-
const styles = StyleSheet.create({
|
|
161
|
-
wrapper: {
|
|
162
|
-
flexDirection: "row",
|
|
163
|
-
alignItems: "flex-start",
|
|
164
|
-
outline: "none",
|
|
165
|
-
},
|
|
166
|
-
label: {
|
|
167
|
-
// NOTE: The checkbox/radio button (height 16px) should be center
|
|
168
|
-
// aligned with the first line of the label. However, LabelMedium has a
|
|
169
|
-
// declared line height of 20px, so we need to adjust the top to get the
|
|
170
|
-
// desired alignment.
|
|
171
|
-
marginTop: -2,
|
|
172
|
-
},
|
|
173
|
-
disabledLabel: {
|
|
174
|
-
color: color.offBlack32,
|
|
175
|
-
},
|
|
176
|
-
description: {
|
|
177
|
-
// 16 for icon + 8 for spacing strut
|
|
178
|
-
marginLeft: spacing.medium_16 + spacing.xSmall_8,
|
|
179
|
-
marginTop: spacing.xxxSmall_4,
|
|
180
|
-
color: color.offBlack64,
|
|
181
|
-
},
|
|
182
|
-
});
|
|
183
|
-
|
|
184
|
-
export default ChoiceInternal;
|