@khanacademy/wonder-blocks-form 4.3.0 → 4.3.2
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 +25 -0
- package/dist/es/index.js +27 -24
- package/dist/index.js +27 -24
- package/package.json +6 -6
- package/src/components/checkbox-core.tsx +70 -76
- package/src/components/checkbox-group.tsx +71 -79
- package/src/components/checkbox.tsx +15 -17
- package/src/components/choice-internal.tsx +93 -93
- package/src/components/choice.tsx +34 -33
- package/src/components/radio-core.tsx +41 -47
- package/src/components/radio-group.tsx +56 -61
- package/src/components/radio.tsx +15 -17
- package/tsconfig-build.tsbuildinfo +1 -1
|
@@ -79,23 +79,21 @@ type ChoiceComponentProps = AriaProps & {
|
|
|
79
79
|
* <Checkbox checked={checked} onChange={setChecked} />
|
|
80
80
|
* ```
|
|
81
81
|
*/
|
|
82
|
-
const Checkbox = React.forwardRef(
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
const {disabled = false, error = false} = props;
|
|
82
|
+
const Checkbox = React.forwardRef(function Checkbox(
|
|
83
|
+
props: ChoiceComponentProps,
|
|
84
|
+
ref: React.ForwardedRef<HTMLInputElement>,
|
|
85
|
+
) {
|
|
86
|
+
const {disabled = false, error = false} = props;
|
|
88
87
|
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
);
|
|
88
|
+
return (
|
|
89
|
+
<ChoiceInternal
|
|
90
|
+
{...props}
|
|
91
|
+
variant="checkbox"
|
|
92
|
+
disabled={disabled}
|
|
93
|
+
error={error}
|
|
94
|
+
ref={ref}
|
|
95
|
+
/>
|
|
96
|
+
);
|
|
97
|
+
});
|
|
100
98
|
|
|
101
99
|
export default Checkbox;
|
|
@@ -55,108 +55,108 @@ type Props = AriaProps & {
|
|
|
55
55
|
* and RadioGroup. This design allows for more explicit prop typing. For
|
|
56
56
|
* example, we can make onChange a required prop on Checkbox but not on Choice
|
|
57
57
|
* (because for Choice, that prop would be auto-populated by CheckboxGroup).
|
|
58
|
-
*/ const ChoiceInternal = React.forwardRef(
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
58
|
+
*/ const ChoiceInternal = React.forwardRef(function ChoiceInternal(
|
|
59
|
+
props: Props,
|
|
60
|
+
ref: React.ForwardedRef<HTMLInputElement>,
|
|
61
|
+
) {
|
|
62
|
+
const {
|
|
63
|
+
checked,
|
|
64
|
+
description,
|
|
65
|
+
disabled = false,
|
|
66
|
+
error = false,
|
|
67
|
+
id,
|
|
68
|
+
label,
|
|
69
|
+
onChange,
|
|
70
|
+
style,
|
|
71
|
+
className,
|
|
72
|
+
variant,
|
|
73
|
+
...coreProps
|
|
74
|
+
} = props;
|
|
75
75
|
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
76
|
+
const handleClick: () => void = () => {
|
|
77
|
+
// Radio buttons cannot be unchecked
|
|
78
|
+
if (variant === "radio" && checked) {
|
|
79
|
+
return;
|
|
80
|
+
}
|
|
81
|
+
onChange(!checked);
|
|
82
|
+
};
|
|
83
83
|
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
84
|
+
const getChoiceCoreComponent = ():
|
|
85
|
+
| typeof RadioCore
|
|
86
|
+
| typeof CheckboxCore => {
|
|
87
|
+
if (variant === "radio") {
|
|
88
|
+
return RadioCore;
|
|
89
|
+
} else {
|
|
90
|
+
return CheckboxCore;
|
|
91
|
+
}
|
|
92
|
+
};
|
|
93
93
|
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
94
|
+
const getLabel = (id: string): React.ReactNode => {
|
|
95
|
+
return (
|
|
96
|
+
<LabelMedium
|
|
97
|
+
style={[styles.label, disabled && styles.disabledLabel]}
|
|
98
|
+
>
|
|
99
|
+
<label htmlFor={id}>{label}</label>
|
|
100
|
+
</LabelMedium>
|
|
101
|
+
);
|
|
102
|
+
};
|
|
103
103
|
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
104
|
+
const getDescription = (id?: string): React.ReactNode => {
|
|
105
|
+
return (
|
|
106
|
+
<LabelSmall style={styles.description} id={id}>
|
|
107
|
+
{description}
|
|
108
|
+
</LabelSmall>
|
|
109
|
+
);
|
|
110
|
+
};
|
|
111
111
|
|
|
112
|
-
|
|
112
|
+
const ChoiceCore = getChoiceCoreComponent();
|
|
113
113
|
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
114
|
+
return (
|
|
115
|
+
<UniqueIDProvider mockOnFirstRender={true} scope="choice">
|
|
116
|
+
{(ids) => {
|
|
117
|
+
// A choice element should always have a unique ID set
|
|
118
|
+
// so that the label can always refer to this element.
|
|
119
|
+
// This guarantees that clicking on the label will
|
|
120
|
+
// always click on the choice as well. If an ID is
|
|
121
|
+
// passed in as a prop, use that one. Otherwise,
|
|
122
|
+
// create a unique ID using the provider.
|
|
123
|
+
const uniqueId = id || ids.get("main");
|
|
124
124
|
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
125
|
+
// Create a unique ID for the description section to be
|
|
126
|
+
// used by this element's `aria-describedby`.
|
|
127
|
+
const descriptionId = description
|
|
128
|
+
? ids.get("description")
|
|
129
|
+
: undefined;
|
|
130
130
|
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
{description && getDescription(descriptionId)}
|
|
131
|
+
return (
|
|
132
|
+
<View style={style} className={className}>
|
|
133
|
+
<View
|
|
134
|
+
style={styles.wrapper}
|
|
135
|
+
// We are resetting the tabIndex=0 from handlers
|
|
136
|
+
// because the ChoiceCore component will receive
|
|
137
|
+
// focus on basis of it being an input element.
|
|
138
|
+
tabIndex={-1}
|
|
139
|
+
>
|
|
140
|
+
<ChoiceCore
|
|
141
|
+
{...coreProps}
|
|
142
|
+
id={uniqueId}
|
|
143
|
+
checked={checked}
|
|
144
|
+
aria-describedby={descriptionId}
|
|
145
|
+
onClick={handleClick}
|
|
146
|
+
disabled={disabled}
|
|
147
|
+
error={error}
|
|
148
|
+
ref={ref}
|
|
149
|
+
/>
|
|
150
|
+
<Strut size={Spacing.xSmall_8} />
|
|
151
|
+
{label && getLabel(uniqueId)}
|
|
153
152
|
</View>
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
);
|
|
153
|
+
{description && getDescription(descriptionId)}
|
|
154
|
+
</View>
|
|
155
|
+
);
|
|
156
|
+
}}
|
|
157
|
+
</UniqueIDProvider>
|
|
158
|
+
);
|
|
159
|
+
});
|
|
160
160
|
|
|
161
161
|
const styles = StyleSheet.create({
|
|
162
162
|
wrapper: {
|
|
@@ -117,40 +117,41 @@ type Props = AriaProps & {
|
|
|
117
117
|
* </RadioGroup>
|
|
118
118
|
* ```
|
|
119
119
|
*/
|
|
120
|
-
const Choice = React.forwardRef(
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
120
|
+
const Choice = React.forwardRef(function Choice(
|
|
121
|
+
props: Props,
|
|
122
|
+
ref: React.ForwardedRef<HTMLInputElement>,
|
|
123
|
+
) {
|
|
124
|
+
const {
|
|
125
|
+
checked = false,
|
|
126
|
+
disabled = false,
|
|
127
|
+
onChange = () => {},
|
|
128
|
+
// we don't need this going into the ChoiceComponent
|
|
129
|
+
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
|
130
|
+
value,
|
|
131
|
+
variant,
|
|
132
|
+
...remainingProps
|
|
133
|
+
} = props;
|
|
132
134
|
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
135
|
+
const getChoiceComponent = (
|
|
136
|
+
variant?: string | null,
|
|
137
|
+
): typeof Radio | typeof Checkbox => {
|
|
138
|
+
if (variant === "checkbox") {
|
|
139
|
+
return Checkbox;
|
|
140
|
+
} else {
|
|
141
|
+
return Radio;
|
|
142
|
+
}
|
|
143
|
+
};
|
|
142
144
|
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
);
|
|
145
|
+
const ChoiceComponent = getChoiceComponent(variant);
|
|
146
|
+
return (
|
|
147
|
+
<ChoiceComponent
|
|
148
|
+
{...remainingProps}
|
|
149
|
+
checked={checked}
|
|
150
|
+
disabled={disabled}
|
|
151
|
+
onChange={onChange}
|
|
152
|
+
ref={ref}
|
|
153
|
+
/>
|
|
154
|
+
);
|
|
155
|
+
});
|
|
155
156
|
|
|
156
157
|
export default Choice;
|
|
@@ -12,53 +12,47 @@ const StyledInput = addStyle("input");
|
|
|
12
12
|
|
|
13
13
|
/**
|
|
14
14
|
* The internal stateless 🔘 Radio button
|
|
15
|
-
*/ const RadioCore = React.forwardRef(
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
/>
|
|
57
|
-
{disabled && checked && <span style={disabledChecked} />}
|
|
58
|
-
</React.Fragment>
|
|
59
|
-
);
|
|
60
|
-
},
|
|
61
|
-
);
|
|
15
|
+
*/ const RadioCore = React.forwardRef(function RadioCore(
|
|
16
|
+
props: ChoiceCoreProps,
|
|
17
|
+
ref: React.ForwardedRef<HTMLInputElement>,
|
|
18
|
+
) {
|
|
19
|
+
const handleChange = () => {
|
|
20
|
+
// Empty because change is handled by ClickableBehavior
|
|
21
|
+
return;
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
const {checked, disabled, error, groupName, id, testId, ...sharedProps} =
|
|
25
|
+
props;
|
|
26
|
+
|
|
27
|
+
const stateStyles = _generateStyles(checked, error);
|
|
28
|
+
const defaultStyle = [
|
|
29
|
+
sharedStyles.inputReset,
|
|
30
|
+
sharedStyles.default,
|
|
31
|
+
!disabled && stateStyles.default,
|
|
32
|
+
disabled && sharedStyles.disabled,
|
|
33
|
+
];
|
|
34
|
+
|
|
35
|
+
return (
|
|
36
|
+
<React.Fragment>
|
|
37
|
+
<StyledInput
|
|
38
|
+
{...sharedProps}
|
|
39
|
+
type="radio"
|
|
40
|
+
aria-invalid={error}
|
|
41
|
+
checked={checked ?? undefined}
|
|
42
|
+
disabled={disabled}
|
|
43
|
+
id={id}
|
|
44
|
+
name={groupName}
|
|
45
|
+
// Need to specify because this is a controlled React form
|
|
46
|
+
// component, but we handle the click via ClickableBehavior
|
|
47
|
+
onChange={handleChange}
|
|
48
|
+
style={defaultStyle}
|
|
49
|
+
data-test-id={testId}
|
|
50
|
+
ref={ref}
|
|
51
|
+
/>
|
|
52
|
+
{disabled && checked && <span style={disabledChecked} />}
|
|
53
|
+
</React.Fragment>
|
|
54
|
+
);
|
|
55
|
+
});
|
|
62
56
|
|
|
63
57
|
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
|
|
64
58
|
const disabledChecked = {
|
|
@@ -96,69 +96,64 @@ const StyledLegend = addStyle("legend");
|
|
|
96
96
|
* </RadioGroup>
|
|
97
97
|
* ```
|
|
98
98
|
*/
|
|
99
|
-
const RadioGroup = React.forwardRef(
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
99
|
+
const RadioGroup = React.forwardRef(function RadioGroup(
|
|
100
|
+
props: RadioGroupProps,
|
|
101
|
+
ref: React.ForwardedRef<HTMLFieldSetElement>,
|
|
102
|
+
) {
|
|
103
|
+
const {
|
|
104
|
+
children,
|
|
105
|
+
label,
|
|
106
|
+
description,
|
|
107
|
+
errorMessage,
|
|
108
|
+
groupName,
|
|
109
|
+
onChange,
|
|
110
|
+
selectedValue,
|
|
111
|
+
style,
|
|
112
|
+
testId,
|
|
113
|
+
} = props;
|
|
112
114
|
|
|
113
|
-
|
|
115
|
+
const allChildren = React.Children.toArray(children).filter(Boolean);
|
|
114
116
|
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
{
|
|
134
|
-
|
|
135
|
-
{errorMessage}
|
|
136
|
-
</LabelSmall>
|
|
137
|
-
)}
|
|
138
|
-
{(label || description || errorMessage) && (
|
|
139
|
-
<Strut size={Spacing.small_12} />
|
|
140
|
-
)}
|
|
117
|
+
return (
|
|
118
|
+
<StyledFieldset data-test-id={testId} style={styles.fieldset} ref={ref}>
|
|
119
|
+
{/* We have a View here because fieldset cannot be used with flexbox*/}
|
|
120
|
+
<View style={style}>
|
|
121
|
+
{label && (
|
|
122
|
+
<StyledLegend style={styles.legend}>
|
|
123
|
+
<LabelMedium>{label}</LabelMedium>
|
|
124
|
+
</StyledLegend>
|
|
125
|
+
)}
|
|
126
|
+
{description && (
|
|
127
|
+
<LabelSmall style={styles.description}>
|
|
128
|
+
{description}
|
|
129
|
+
</LabelSmall>
|
|
130
|
+
)}
|
|
131
|
+
{errorMessage && (
|
|
132
|
+
<LabelSmall style={styles.error}>{errorMessage}</LabelSmall>
|
|
133
|
+
)}
|
|
134
|
+
{(label || description || errorMessage) && (
|
|
135
|
+
<Strut size={Spacing.small_12} />
|
|
136
|
+
)}
|
|
141
137
|
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
);
|
|
138
|
+
{allChildren.map((child, index) => {
|
|
139
|
+
// @ts-expect-error [FEI-5019] - TS2339 - Property 'props' does not exist on type 'ReactChild | ReactFragment | ReactPortal'.
|
|
140
|
+
const {style, value} = child.props;
|
|
141
|
+
const checked = selectedValue === value;
|
|
142
|
+
// @ts-expect-error [FEI-5019] - TS2769 - No overload matches this call.
|
|
143
|
+
return React.cloneElement(child, {
|
|
144
|
+
checked: checked,
|
|
145
|
+
error: !!errorMessage,
|
|
146
|
+
groupName: groupName,
|
|
147
|
+
id: `${groupName}-${value}`,
|
|
148
|
+
key: value,
|
|
149
|
+
onChange: () => onChange(value),
|
|
150
|
+
style: [index > 0 && styles.defaultLineGap, style],
|
|
151
|
+
variant: "radio",
|
|
152
|
+
});
|
|
153
|
+
})}
|
|
154
|
+
</View>
|
|
155
|
+
</StyledFieldset>
|
|
156
|
+
);
|
|
157
|
+
});
|
|
163
158
|
|
|
164
159
|
export default RadioGroup;
|
package/src/components/radio.tsx
CHANGED
|
@@ -62,23 +62,21 @@ type ChoiceComponentProps = AriaProps & {
|
|
|
62
62
|
*
|
|
63
63
|
* This component should not really be used by itself because radio buttons are
|
|
64
64
|
* often grouped together. See RadioGroup.
|
|
65
|
-
*/ const Radio = React.forwardRef(
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
const {disabled = false, error = false, ...otherProps} = props;
|
|
65
|
+
*/ const Radio = React.forwardRef(function Radio(
|
|
66
|
+
props: ChoiceComponentProps,
|
|
67
|
+
ref: React.ForwardedRef<HTMLInputElement>,
|
|
68
|
+
) {
|
|
69
|
+
const {disabled = false, error = false, ...otherProps} = props;
|
|
71
70
|
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
);
|
|
71
|
+
return (
|
|
72
|
+
<ChoiceInternal
|
|
73
|
+
{...otherProps}
|
|
74
|
+
variant="radio"
|
|
75
|
+
disabled={disabled}
|
|
76
|
+
error={error}
|
|
77
|
+
ref={ref}
|
|
78
|
+
/>
|
|
79
|
+
);
|
|
80
|
+
});
|
|
83
81
|
|
|
84
82
|
export default Radio;
|