@khanacademy/wonder-blocks-form 4.0.8 → 4.1.0
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 +4 -8
- package/dist/components/checkbox-core.js.flow +7 -10
- package/dist/components/checkbox.d.ts +3 -2
- package/dist/components/checkbox.js.flow +3 -2
- package/dist/components/choice-internal.d.ts +2 -4
- package/dist/components/choice-internal.js.flow +2 -4
- package/dist/components/radio-core.d.ts +1 -8
- package/dist/components/radio-core.js.flow +4 -10
- package/dist/es/index.js +123 -97
- package/dist/index.js +122 -96
- package/dist/util/types.d.ts +4 -1
- package/dist/util/types.js.flow +7 -1
- package/package.json +6 -6
- package/src/__tests__/__snapshots__/custom-snapshot.test.tsx.snap +261 -743
- package/src/__tests__/custom-snapshot.test.tsx +29 -44
- package/src/components/__tests__/checkbox.test.js +84 -0
- package/src/components/__tests__/labeled-text-field.test.tsx +4 -1
- package/src/components/checkbox-core.tsx +91 -49
- package/src/components/checkbox-group.tsx +2 -2
- package/src/components/checkbox.tsx +4 -2
- package/src/components/choice-internal.tsx +32 -42
- package/src/components/radio-core.tsx +41 -37
- package/src/components/radio-group.tsx +2 -2
- package/src/components/text-field.tsx +13 -8
- package/src/util/types.ts +6 -1
- package/tsconfig.tsbuildinfo +1 -1
|
@@ -5,32 +5,24 @@ import CheckboxCore from "../components/checkbox-core";
|
|
|
5
5
|
import RadioCore from "../components/radio-core";
|
|
6
6
|
|
|
7
7
|
const states = ["default", "error", "disabled"];
|
|
8
|
-
const
|
|
9
|
-
const checkedStates = [false, true];
|
|
8
|
+
const checkedStates = [false, true, null];
|
|
10
9
|
|
|
11
10
|
describe("CheckboxCore", () => {
|
|
12
11
|
states.forEach((state: any) => {
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
focused={clickableState === "focused"}
|
|
28
|
-
waiting={false}
|
|
29
|
-
/>,
|
|
30
|
-
)
|
|
31
|
-
.toJSON();
|
|
32
|
-
expect(tree).toMatchSnapshot();
|
|
33
|
-
});
|
|
12
|
+
checkedStates.forEach((checked: any) => {
|
|
13
|
+
test(`type:${state} checked:${String(checked)}`, () => {
|
|
14
|
+
const disabled = state === "disabled";
|
|
15
|
+
const tree = renderer
|
|
16
|
+
.create(
|
|
17
|
+
<CheckboxCore
|
|
18
|
+
checked={checked}
|
|
19
|
+
disabled={disabled}
|
|
20
|
+
error={state === "error"}
|
|
21
|
+
onClick={() => {}}
|
|
22
|
+
/>,
|
|
23
|
+
)
|
|
24
|
+
.toJSON();
|
|
25
|
+
expect(tree).toMatchSnapshot();
|
|
34
26
|
});
|
|
35
27
|
});
|
|
36
28
|
});
|
|
@@ -38,27 +30,20 @@ describe("CheckboxCore", () => {
|
|
|
38
30
|
|
|
39
31
|
describe("RadioCore", () => {
|
|
40
32
|
states.forEach((state: any) => {
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
focused={clickableState === "focused"}
|
|
56
|
-
waiting={false}
|
|
57
|
-
/>,
|
|
58
|
-
)
|
|
59
|
-
.toJSON();
|
|
60
|
-
expect(tree).toMatchSnapshot();
|
|
61
|
-
});
|
|
33
|
+
checkedStates.forEach((checked: any) => {
|
|
34
|
+
test(`type:${state} checked:${String(checked)}`, () => {
|
|
35
|
+
const disabled = state === "disabled";
|
|
36
|
+
const tree = renderer
|
|
37
|
+
.create(
|
|
38
|
+
<RadioCore
|
|
39
|
+
checked={checked}
|
|
40
|
+
disabled={disabled}
|
|
41
|
+
error={state === "error"}
|
|
42
|
+
onClick={() => {}}
|
|
43
|
+
/>,
|
|
44
|
+
)
|
|
45
|
+
.toJSON();
|
|
46
|
+
expect(tree).toMatchSnapshot();
|
|
62
47
|
});
|
|
63
48
|
});
|
|
64
49
|
});
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
//@flow
|
|
2
|
+
import * as React from "react";
|
|
3
|
+
import {render, screen} from "@testing-library/react";
|
|
4
|
+
|
|
5
|
+
import Checkbox from "../checkbox";
|
|
6
|
+
|
|
7
|
+
describe("Checkbox", () => {
|
|
8
|
+
test("uses the ID prop when it is specified", () => {
|
|
9
|
+
// Arrange, Act
|
|
10
|
+
render(
|
|
11
|
+
<Checkbox
|
|
12
|
+
id="specified-checkbox-id"
|
|
13
|
+
label="Receive assignment reminders for Algebra"
|
|
14
|
+
description="You will receive a reminder 24 hours before each deadline"
|
|
15
|
+
checked={false}
|
|
16
|
+
onChange={() => {}}
|
|
17
|
+
/>,
|
|
18
|
+
);
|
|
19
|
+
|
|
20
|
+
const checkbox = screen.getByRole("checkbox");
|
|
21
|
+
|
|
22
|
+
// Assert
|
|
23
|
+
expect(checkbox).toHaveAttribute("id", "specified-checkbox-id");
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
test("provides a unique ID when the ID prop is not specified", () => {
|
|
27
|
+
// Arrange, Act
|
|
28
|
+
render(
|
|
29
|
+
<Checkbox
|
|
30
|
+
label="Receive assignment reminders for Algebra"
|
|
31
|
+
description="You will receive a reminder 24 hours before each deadline"
|
|
32
|
+
checked={false}
|
|
33
|
+
onChange={() => {}}
|
|
34
|
+
/>,
|
|
35
|
+
);
|
|
36
|
+
|
|
37
|
+
const checkbox = screen.getByRole("checkbox");
|
|
38
|
+
|
|
39
|
+
// Assert
|
|
40
|
+
expect(checkbox).toHaveAttribute("id", "uid-choice-1-main");
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
test("clicking the checkbox triggers `onChange`", () => {
|
|
44
|
+
// Arrange
|
|
45
|
+
const onChangeSpy = jest.fn();
|
|
46
|
+
render(
|
|
47
|
+
<Checkbox
|
|
48
|
+
label="Receive assignment reminders for Algebra"
|
|
49
|
+
description="You will receive a reminder 24 hours before each deadline"
|
|
50
|
+
checked={false}
|
|
51
|
+
onChange={onChangeSpy}
|
|
52
|
+
/>,
|
|
53
|
+
);
|
|
54
|
+
|
|
55
|
+
// Act
|
|
56
|
+
const checkbox = screen.getByRole("checkbox");
|
|
57
|
+
checkbox.click();
|
|
58
|
+
|
|
59
|
+
// Assert
|
|
60
|
+
expect(onChangeSpy).toHaveBeenCalled();
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
test("clicks the label triggers `onChange`", () => {
|
|
64
|
+
// Arrange
|
|
65
|
+
const onChangeSpy = jest.fn();
|
|
66
|
+
render(
|
|
67
|
+
<Checkbox
|
|
68
|
+
label="Receive assignment reminders for Algebra"
|
|
69
|
+
description="You will receive a reminder 24 hours before each deadline"
|
|
70
|
+
checked={false}
|
|
71
|
+
onChange={onChangeSpy}
|
|
72
|
+
/>,
|
|
73
|
+
);
|
|
74
|
+
|
|
75
|
+
// Act
|
|
76
|
+
const checkboxLabel = screen.getByText(
|
|
77
|
+
"Receive assignment reminders for Algebra",
|
|
78
|
+
);
|
|
79
|
+
checkboxLabel.click();
|
|
80
|
+
|
|
81
|
+
// Assert
|
|
82
|
+
expect(onChangeSpy).toHaveBeenCalled();
|
|
83
|
+
});
|
|
84
|
+
});
|
|
@@ -3,6 +3,7 @@ import {render, screen, fireEvent} from "@testing-library/react";
|
|
|
3
3
|
import userEvent from "@testing-library/user-event";
|
|
4
4
|
|
|
5
5
|
import {StyleSheet} from "aphrodite";
|
|
6
|
+
import Color from "@khanacademy/wonder-blocks-color";
|
|
6
7
|
import LabeledTextField from "../labeled-text-field";
|
|
7
8
|
|
|
8
9
|
describe("LabeledTextField", () => {
|
|
@@ -392,7 +393,9 @@ describe("LabeledTextField", () => {
|
|
|
392
393
|
textField.focus();
|
|
393
394
|
|
|
394
395
|
// Assert
|
|
395
|
-
expect(textField
|
|
396
|
+
expect(textField).toHaveStyle({
|
|
397
|
+
boxShadow: `0px 0px 0px 1px ${Color.blue}, 0px 0px 0px 2px ${Color.white}`,
|
|
398
|
+
});
|
|
396
399
|
});
|
|
397
400
|
|
|
398
401
|
it("style prop is passed to fieldheading", async () => {
|
|
@@ -6,28 +6,54 @@ import {addStyle} from "@khanacademy/wonder-blocks-core";
|
|
|
6
6
|
import Icon from "@khanacademy/wonder-blocks-icon";
|
|
7
7
|
|
|
8
8
|
import type {IconAsset} from "@khanacademy/wonder-blocks-icon";
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
9
|
+
import type {ChoiceCoreProps, Checked} from "../util/types";
|
|
10
|
+
|
|
11
|
+
// `AriaChecked` and `mapCheckedToAriaChecked()` are used to convert the
|
|
12
|
+
// `checked` prop value to a value that a screen reader can understand via the
|
|
13
|
+
// `aria-checked` attribute
|
|
14
|
+
type AriaChecked = "true" | "false" | "mixed";
|
|
15
|
+
|
|
16
|
+
function mapCheckedToAriaChecked(value: Checked): AriaChecked {
|
|
17
|
+
switch (value) {
|
|
18
|
+
case true:
|
|
19
|
+
return "true";
|
|
20
|
+
case false:
|
|
21
|
+
return "false";
|
|
22
|
+
default:
|
|
23
|
+
return "mixed";
|
|
24
|
+
}
|
|
25
|
+
}
|
|
18
26
|
|
|
19
27
|
const {blue, red, white, offWhite, offBlack16, offBlack32, offBlack50} = Color;
|
|
20
28
|
|
|
21
29
|
const StyledInput = addStyle("input");
|
|
22
30
|
|
|
23
|
-
const
|
|
31
|
+
const checkPath: IconAsset = {
|
|
24
32
|
small: "M11.263 4.324a1 1 0 1 1 1.474 1.352l-5.5 6a1 1 0 0 1-1.505-.036l-2.5-3a1 1 0 1 1 1.536-1.28L6.536 9.48l4.727-5.157z",
|
|
25
33
|
};
|
|
26
34
|
|
|
35
|
+
const indeterminatePath: IconAsset = {
|
|
36
|
+
small: "M3 8C3 7.44772 3.44772 7 4 7H12C12.5523 7 13 7.44772 13 8C13 8.55228 12.5523 9 12 9H4C3.44772 9 3 8.55228 3 8Z",
|
|
37
|
+
};
|
|
38
|
+
|
|
27
39
|
/**
|
|
28
40
|
* The internal stateless ☑️ Checkbox
|
|
29
41
|
*/
|
|
30
|
-
export default class CheckboxCore extends React.Component<
|
|
42
|
+
export default class CheckboxCore extends React.Component<ChoiceCoreProps> {
|
|
43
|
+
componentDidMount(): void {
|
|
44
|
+
if (this.props.checked == null && this.inputRef.current != null) {
|
|
45
|
+
this.inputRef.current.indeterminate = true;
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
componentDidUpdate(prevProps: Readonly<ChoiceCoreProps>): void {
|
|
50
|
+
if (this.inputRef.current != null) {
|
|
51
|
+
this.inputRef.current.indeterminate = this.props.checked == null;
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
inputRef: React.RefObject<HTMLInputElement> = React.createRef();
|
|
56
|
+
|
|
31
57
|
handleChange: () => void = () => {
|
|
32
58
|
// Empty because change is handled by ClickableBehavior
|
|
33
59
|
return;
|
|
@@ -41,10 +67,6 @@ export default class CheckboxCore extends React.Component<Props> {
|
|
|
41
67
|
groupName,
|
|
42
68
|
id,
|
|
43
69
|
testId,
|
|
44
|
-
hovered,
|
|
45
|
-
focused,
|
|
46
|
-
pressed,
|
|
47
|
-
waiting: _,
|
|
48
70
|
...sharedProps
|
|
49
71
|
} = this.props;
|
|
50
72
|
|
|
@@ -53,11 +75,7 @@ export default class CheckboxCore extends React.Component<Props> {
|
|
|
53
75
|
const defaultStyle = [
|
|
54
76
|
sharedStyles.inputReset,
|
|
55
77
|
sharedStyles.default,
|
|
56
|
-
stateStyles.default,
|
|
57
|
-
!disabled &&
|
|
58
|
-
(pressed
|
|
59
|
-
? stateStyles.active
|
|
60
|
-
: (hovered || focused) && stateStyles.focus),
|
|
78
|
+
!disabled && stateStyles.default,
|
|
61
79
|
disabled && sharedStyles.disabled,
|
|
62
80
|
];
|
|
63
81
|
|
|
@@ -65,13 +83,26 @@ export default class CheckboxCore extends React.Component<Props> {
|
|
|
65
83
|
"data-test-id": testId,
|
|
66
84
|
} as const;
|
|
67
85
|
|
|
86
|
+
const checkboxIcon = (
|
|
87
|
+
<Icon
|
|
88
|
+
color={disabled ? offBlack32 : white}
|
|
89
|
+
icon={checked ? checkPath : indeterminatePath}
|
|
90
|
+
size="small"
|
|
91
|
+
style={sharedStyles.checkboxIcon}
|
|
92
|
+
/>
|
|
93
|
+
);
|
|
94
|
+
|
|
95
|
+
const ariaChecked = mapCheckedToAriaChecked(checked);
|
|
96
|
+
|
|
68
97
|
return (
|
|
69
98
|
<React.Fragment>
|
|
70
99
|
<StyledInput
|
|
71
100
|
{...sharedProps}
|
|
101
|
+
ref={this.inputRef}
|
|
72
102
|
type="checkbox"
|
|
103
|
+
aria-checked={ariaChecked}
|
|
73
104
|
aria-invalid={error}
|
|
74
|
-
checked={checked}
|
|
105
|
+
checked={checked ?? undefined}
|
|
75
106
|
disabled={disabled}
|
|
76
107
|
id={id}
|
|
77
108
|
name={groupName}
|
|
@@ -81,14 +112,7 @@ export default class CheckboxCore extends React.Component<Props> {
|
|
|
81
112
|
style={defaultStyle}
|
|
82
113
|
{...props}
|
|
83
114
|
/>
|
|
84
|
-
{checked
|
|
85
|
-
<Icon
|
|
86
|
-
color={disabled ? offBlack32 : white}
|
|
87
|
-
icon={checkboxCheck}
|
|
88
|
-
size="small"
|
|
89
|
-
style={sharedStyles.checkIcon}
|
|
90
|
-
/>
|
|
91
|
-
)}
|
|
115
|
+
{checked || checked == null ? checkboxIcon : <></>}
|
|
92
116
|
</React.Fragment>
|
|
93
117
|
);
|
|
94
118
|
}
|
|
@@ -124,7 +148,7 @@ const sharedStyles = StyleSheet.create({
|
|
|
124
148
|
borderWidth: 1,
|
|
125
149
|
},
|
|
126
150
|
|
|
127
|
-
|
|
151
|
+
checkboxIcon: {
|
|
128
152
|
position: "absolute",
|
|
129
153
|
pointerEvents: "none",
|
|
130
154
|
},
|
|
@@ -150,7 +174,7 @@ const colors = {
|
|
|
150
174
|
|
|
151
175
|
const styles: Record<string, any> = {};
|
|
152
176
|
|
|
153
|
-
const _generateStyles = (checked:
|
|
177
|
+
const _generateStyles = (checked: Checked, error: boolean) => {
|
|
154
178
|
// "hash" the parameters
|
|
155
179
|
const styleKey = `${String(checked)}-${String(error)}`;
|
|
156
180
|
if (styles[styleKey]) {
|
|
@@ -160,18 +184,26 @@ const _generateStyles = (checked: boolean, error: boolean) => {
|
|
|
160
184
|
const palette = error ? colors.error : colors.default;
|
|
161
185
|
|
|
162
186
|
let newStyles: Record<string, any> = {};
|
|
163
|
-
if (checked) {
|
|
187
|
+
if (checked || checked == null) {
|
|
164
188
|
newStyles = {
|
|
165
189
|
default: {
|
|
166
190
|
backgroundColor: palette.base,
|
|
167
191
|
borderWidth: 0,
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
192
|
+
|
|
193
|
+
// Focus and hover have the same style. Focus style only shows
|
|
194
|
+
// up with keyboard navigation.
|
|
195
|
+
":focus-visible": {
|
|
196
|
+
boxShadow: `0 0 0 1px ${white}, 0 0 0 3px ${palette.base}`,
|
|
197
|
+
},
|
|
198
|
+
|
|
199
|
+
":hover": {
|
|
200
|
+
boxShadow: `0 0 0 1px ${white}, 0 0 0 3px ${palette.base}`,
|
|
201
|
+
},
|
|
202
|
+
|
|
203
|
+
":active": {
|
|
204
|
+
boxShadow: `0 0 0 1px ${white}, 0 0 0 3px ${palette.active}`,
|
|
205
|
+
background: palette.active,
|
|
206
|
+
},
|
|
175
207
|
},
|
|
176
208
|
};
|
|
177
209
|
} else {
|
|
@@ -179,16 +211,26 @@ const _generateStyles = (checked: boolean, error: boolean) => {
|
|
|
179
211
|
default: {
|
|
180
212
|
backgroundColor: error ? fadedRed : white,
|
|
181
213
|
borderColor: error ? red : offBlack50,
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
214
|
+
|
|
215
|
+
// Focus and hover have the same style. Focus style only shows
|
|
216
|
+
// up with keyboard navigation.
|
|
217
|
+
":focus-visible": {
|
|
218
|
+
backgroundColor: error ? fadedRed : white,
|
|
219
|
+
borderColor: palette.base,
|
|
220
|
+
borderWidth: 2,
|
|
221
|
+
},
|
|
222
|
+
|
|
223
|
+
":hover": {
|
|
224
|
+
backgroundColor: error ? fadedRed : white,
|
|
225
|
+
borderColor: palette.base,
|
|
226
|
+
borderWidth: 2,
|
|
227
|
+
},
|
|
228
|
+
|
|
229
|
+
":active": {
|
|
230
|
+
backgroundColor: palette.faded,
|
|
231
|
+
borderColor: error ? activeRed : blue,
|
|
232
|
+
borderWidth: 2,
|
|
233
|
+
},
|
|
192
234
|
},
|
|
193
235
|
};
|
|
194
236
|
}
|
|
@@ -59,8 +59,8 @@ type CheckboxGroupProps = {
|
|
|
59
59
|
testId?: string;
|
|
60
60
|
};
|
|
61
61
|
|
|
62
|
-
const StyledFieldset = addStyle
|
|
63
|
-
const StyledLegend = addStyle
|
|
62
|
+
const StyledFieldset = addStyle("fieldset");
|
|
63
|
+
const StyledLegend = addStyle("legend");
|
|
64
64
|
|
|
65
65
|
/**
|
|
66
66
|
* A checkbox group allows multiple selection. This component auto-populates
|
|
@@ -1,14 +1,16 @@
|
|
|
1
1
|
import * as React from "react";
|
|
2
2
|
|
|
3
3
|
import type {AriaProps, StyleType} from "@khanacademy/wonder-blocks-core";
|
|
4
|
+
import type {Checked} from "../util/types";
|
|
5
|
+
|
|
4
6
|
import ChoiceInternal from "./choice-internal";
|
|
5
7
|
|
|
6
8
|
// Keep synced with ChoiceComponentProps in ../util/types.js
|
|
7
9
|
type ChoiceComponentProps = AriaProps & {
|
|
8
10
|
/**
|
|
9
|
-
* Whether this component is checked
|
|
11
|
+
* Whether this component is checked or indeterminate
|
|
10
12
|
*/
|
|
11
|
-
checked:
|
|
13
|
+
checked: Checked;
|
|
12
14
|
/**
|
|
13
15
|
* Whether this component is disabled
|
|
14
16
|
*/
|
|
@@ -3,7 +3,6 @@ import {StyleSheet} from "aphrodite";
|
|
|
3
3
|
|
|
4
4
|
import Color from "@khanacademy/wonder-blocks-color";
|
|
5
5
|
import {View, UniqueIDProvider} from "@khanacademy/wonder-blocks-core";
|
|
6
|
-
import {getClickableBehavior} from "@khanacademy/wonder-blocks-clickable";
|
|
7
6
|
import {Strut} from "@khanacademy/wonder-blocks-layout";
|
|
8
7
|
import Spacing from "@khanacademy/wonder-blocks-spacing";
|
|
9
8
|
import {LabelMedium, LabelSmall} from "@khanacademy/wonder-blocks-typography";
|
|
@@ -13,7 +12,7 @@ import RadioCore from "./radio-core";
|
|
|
13
12
|
|
|
14
13
|
type Props = AriaProps & {
|
|
15
14
|
/** Whether this choice is checked. */
|
|
16
|
-
checked: boolean;
|
|
15
|
+
checked: boolean | null | undefined;
|
|
17
16
|
/** Whether this choice option is disabled. */
|
|
18
17
|
disabled: boolean;
|
|
19
18
|
/** Whether this choice is in error mode. */
|
|
@@ -50,7 +49,6 @@ type Props = AriaProps & {
|
|
|
50
49
|
};
|
|
51
50
|
|
|
52
51
|
type DefaultProps = {
|
|
53
|
-
checked: Props["checked"];
|
|
54
52
|
disabled: Props["disabled"];
|
|
55
53
|
error: Props["error"];
|
|
56
54
|
};
|
|
@@ -64,17 +62,10 @@ type DefaultProps = {
|
|
|
64
62
|
* (because for Choice, that prop would be auto-populated by CheckboxGroup).
|
|
65
63
|
*/ export default class ChoiceInternal extends React.Component<Props> {
|
|
66
64
|
static defaultProps: DefaultProps = {
|
|
67
|
-
checked: false,
|
|
68
65
|
disabled: false,
|
|
69
66
|
error: false,
|
|
70
67
|
};
|
|
71
68
|
|
|
72
|
-
handleLabelClick: (event: React.SyntheticEvent) => void = (event) => {
|
|
73
|
-
// Browsers automatically use the for attribute to select the input,
|
|
74
|
-
// but we use ClickableBehavior to handle this.
|
|
75
|
-
event.preventDefault();
|
|
76
|
-
};
|
|
77
|
-
|
|
78
69
|
handleClick: () => void = () => {
|
|
79
70
|
const {checked, onChange, variant} = this.props;
|
|
80
71
|
// Radio buttons cannot be unchecked
|
|
@@ -91,15 +82,13 @@ type DefaultProps = {
|
|
|
91
82
|
return CheckboxCore;
|
|
92
83
|
}
|
|
93
84
|
}
|
|
94
|
-
getLabel(): React.ReactNode {
|
|
95
|
-
const {disabled,
|
|
85
|
+
getLabel(id: string): React.ReactNode {
|
|
86
|
+
const {disabled, label} = this.props;
|
|
96
87
|
return (
|
|
97
88
|
<LabelMedium
|
|
98
89
|
style={[styles.label, disabled && styles.disabledLabel]}
|
|
99
90
|
>
|
|
100
|
-
<label htmlFor={id}
|
|
101
|
-
{label}
|
|
102
|
-
</label>
|
|
91
|
+
<label htmlFor={id}>{label}</label>
|
|
103
92
|
</LabelMedium>
|
|
104
93
|
);
|
|
105
94
|
}
|
|
@@ -115,18 +104,30 @@ type DefaultProps = {
|
|
|
115
104
|
const {
|
|
116
105
|
label,
|
|
117
106
|
description,
|
|
107
|
+
id,
|
|
118
108
|
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
|
119
109
|
onChange,
|
|
120
110
|
style,
|
|
121
111
|
className,
|
|
112
|
+
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
|
122
113
|
variant,
|
|
123
114
|
...coreProps
|
|
124
115
|
} = this.props;
|
|
125
116
|
const ChoiceCore = this.getChoiceCoreComponent();
|
|
126
|
-
|
|
117
|
+
|
|
127
118
|
return (
|
|
128
119
|
<UniqueIDProvider mockOnFirstRender={true} scope="choice">
|
|
129
120
|
{(ids) => {
|
|
121
|
+
// A choice element should always have a unique ID set
|
|
122
|
+
// so that the label can always refer to this element.
|
|
123
|
+
// This guarantees that clicking on the label will
|
|
124
|
+
// always click on the choice as well. If an ID is
|
|
125
|
+
// passed in as a prop, use that one. Otherwise,
|
|
126
|
+
// create a unique ID using the provider.
|
|
127
|
+
const uniqueId = id || ids.get("main");
|
|
128
|
+
|
|
129
|
+
// Create a unique ID for the description section to be
|
|
130
|
+
// used by this element's `aria-describedby`.
|
|
130
131
|
const descriptionId = description
|
|
131
132
|
? ids.get("description")
|
|
132
133
|
: undefined;
|
|
@@ -134,32 +135,22 @@ type DefaultProps = {
|
|
|
134
135
|
return (
|
|
135
136
|
// @ts-expect-error [FEI-5019] - TS2769 - No overload matches this call.
|
|
136
137
|
<View style={style} className={className}>
|
|
137
|
-
<
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
138
|
+
<View
|
|
139
|
+
style={styles.wrapper}
|
|
140
|
+
// We are resetting the tabIndex=0 from handlers
|
|
141
|
+
// because the ChoiceCore component will receive
|
|
142
|
+
// focus on basis of it being an input element.
|
|
143
|
+
tabIndex={-1}
|
|
141
144
|
>
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
>
|
|
152
|
-
<ChoiceCore
|
|
153
|
-
{...coreProps}
|
|
154
|
-
{...state}
|
|
155
|
-
aria-describedby={descriptionId}
|
|
156
|
-
/>
|
|
157
|
-
<Strut size={Spacing.xSmall_8} />
|
|
158
|
-
{label && this.getLabel()}
|
|
159
|
-
</View>
|
|
160
|
-
);
|
|
161
|
-
}}
|
|
162
|
-
</ClickableBehavior>
|
|
145
|
+
<ChoiceCore
|
|
146
|
+
{...coreProps}
|
|
147
|
+
id={uniqueId}
|
|
148
|
+
aria-describedby={descriptionId}
|
|
149
|
+
onClick={this.handleClick}
|
|
150
|
+
/>
|
|
151
|
+
<Strut size={Spacing.xSmall_8} />
|
|
152
|
+
{label && this.getLabel(uniqueId)}
|
|
153
|
+
</View>
|
|
163
154
|
{description && this.getDescription(descriptionId)}
|
|
164
155
|
</View>
|
|
165
156
|
);
|
|
@@ -175,7 +166,6 @@ const styles = StyleSheet.create({
|
|
|
175
166
|
outline: "none",
|
|
176
167
|
},
|
|
177
168
|
label: {
|
|
178
|
-
userSelect: "none",
|
|
179
169
|
// NOTE: The checkbox/radio button (height 16px) should be center
|
|
180
170
|
// aligned with the first line of the label. However, LabelMedium has a
|
|
181
171
|
// declared line height of 20px, so we need to adjust the top to get the
|