@khanacademy/wonder-blocks-form 2.4.8 → 3.0.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.
Files changed (31) hide show
  1. package/CHANGELOG.md +12 -0
  2. package/dist/es/index.js +11 -11
  3. package/dist/index.js +71 -75
  4. package/docs.md +5 -1
  5. package/package.json +2 -2
  6. package/src/__docs__/_overview_.stories.mdx +15 -0
  7. package/src/components/__docs__/checkbox-group.stories.js +0 -1
  8. package/src/components/__docs__/labeled-text-field.argtypes.js +2 -2
  9. package/src/components/__docs__/labeled-text-field.stories.js +25 -0
  10. package/src/components/__docs__/radio.stories.js +3 -2
  11. package/src/components/__tests__/checkbox-group.test.js +118 -67
  12. package/src/components/__tests__/field-heading.test.js +40 -0
  13. package/src/components/__tests__/radio-group.test.js +131 -58
  14. package/src/components/checkbox-group.js +5 -13
  15. package/src/components/checkbox.js +2 -2
  16. package/src/components/choice-internal.js +5 -3
  17. package/src/components/choice.js +2 -2
  18. package/src/components/field-heading.js +27 -43
  19. package/src/components/labeled-text-field.js +2 -3
  20. package/src/components/radio-group.js +2 -2
  21. package/src/components/radio.js +2 -2
  22. package/src/index.js +0 -2
  23. package/src/__tests__/__snapshots__/generated-snapshot.test.js.snap +0 -6126
  24. package/src/__tests__/generated-snapshot.test.js +0 -654
  25. package/src/components/checkbox-group.md +0 -200
  26. package/src/components/checkbox.md +0 -134
  27. package/src/components/field-heading.md +0 -43
  28. package/src/components/labeled-text-field.md +0 -535
  29. package/src/components/radio-group.md +0 -129
  30. package/src/components/radio.md +0 -26
  31. package/src/components/text-field.md +0 -770
@@ -1,85 +1,136 @@
1
1
  //@flow
2
2
  import * as React from "react";
3
- import {mount} from "enzyme";
4
- import "jest-enzyme";
3
+ import {render, screen} from "@testing-library/react";
4
+ import userEvent from "@testing-library/user-event";
5
5
 
6
6
  import CheckboxGroup from "../checkbox-group.js";
7
7
  import Choice from "../choice.js";
8
8
 
9
9
  describe("CheckboxGroup", () => {
10
- let group;
11
- const onChange = jest.fn();
12
-
13
- beforeEach(() => {
14
- group = mount(
15
- <CheckboxGroup
16
- label="Test"
17
- description="test description"
18
- groupName="test"
19
- onChange={onChange}
20
- selectedValues={["a", "b"]}
21
- >
22
- <Choice label="a" value="a" aria-labelledby="test-a" />
23
- <Choice label="b" value="b" aria-labelledby="test-b" />
24
- <Choice label="c" value="c" aria-labelledby="test-c" />
25
- </CheckboxGroup>,
26
- );
27
- });
10
+ describe("behavior", () => {
11
+ const TestComponent = ({errorMessage}: {|errorMessage?: string|}) => {
12
+ const [selectedValues, setSelectedValue] = React.useState([
13
+ "a",
14
+ "b",
15
+ ]);
16
+ const handleChange = (selectedValues) => {
17
+ setSelectedValue(selectedValues);
18
+ };
19
+ return (
20
+ <CheckboxGroup
21
+ label="Test"
22
+ description="test description"
23
+ groupName="test"
24
+ onChange={handleChange}
25
+ selectedValues={selectedValues}
26
+ errorMessage={errorMessage}
27
+ >
28
+ <Choice label="a" value="a" aria-labelledby="test-a" />
29
+ <Choice label="b" value="b" aria-labelledby="test-b" />
30
+ <Choice label="c" value="c" aria-labelledby="test-c" />
31
+ </CheckboxGroup>
32
+ );
33
+ };
28
34
 
29
- it("has the correct items checked", () => {
30
- const a = group.find(Choice).at(0);
31
- const b = group.find(Choice).at(1);
32
- const c = group.find(Choice).at(2);
35
+ it("has the correct items checked", () => {
36
+ // Arrange, Act
37
+ render(<TestComponent />);
33
38
 
34
- // a starts off checked
35
- expect(a.prop("checked")).toEqual(true);
36
- expect(b.prop("checked")).toEqual(true);
37
- expect(c.prop("checked")).toEqual(false);
38
- });
39
+ const checkboxes = screen.getAllByRole("checkbox");
39
40
 
40
- it("changes selection when selectedValue changes", () => {
41
- group.setProps({selectedValues: ["b"]});
42
- const a = group.find(Choice).at(0);
43
- const b = group.find(Choice).at(1);
44
- const c = group.find(Choice).at(2);
41
+ // Assert
42
+ // a starts off checked
43
+ expect(checkboxes[0]).toBeChecked();
44
+ expect(checkboxes[1]).toBeChecked();
45
+ expect(checkboxes[2]).not.toBeChecked();
46
+ });
45
47
 
46
- // now only b is checked
47
- expect(a.prop("checked")).toEqual(false);
48
- expect(b.prop("checked")).toEqual(true);
49
- expect(c.prop("checked")).toEqual(false);
50
- });
48
+ it("clicking a selected choice deselects it", () => {
49
+ // Arrange
50
+ render(<TestComponent />);
51
51
 
52
- it("displays error state for all Choice children", () => {
53
- group.setProps({errorMessage: "there's an error"});
54
- const a = group.find(Choice).at(0);
55
- const b = group.find(Choice).at(1);
56
- const c = group.find(Choice).at(2);
52
+ const checkboxes = screen.getAllByRole("checkbox");
57
53
 
58
- expect(a.prop("error")).toEqual(true);
59
- expect(b.prop("error")).toEqual(true);
60
- expect(c.prop("error")).toEqual(true);
61
- });
54
+ // Act
55
+ userEvent.click(checkboxes[0]);
56
+
57
+ // Assert
58
+ expect(checkboxes[0]).not.toBeChecked();
59
+ expect(checkboxes[1]).toBeChecked();
60
+ expect(checkboxes[2]).not.toBeChecked();
61
+ });
62
+
63
+ it("should set aria-invalid on choices when there's an error message", () => {
64
+ // Arrange, Act
65
+ render(<TestComponent errorMessage="there's an error" />);
66
+
67
+ const checkboxes = screen.getAllByRole("checkbox");
62
68
 
63
- it("calls onChange for each new selection", () => {
64
- // a is clicked
65
- const a = group.find(Choice).at(0);
66
- const aTarget = a.find("ClickableBehavior");
67
- aTarget.simulate("click");
68
- expect(onChange).toHaveBeenCalledTimes(1);
69
-
70
- // now b is clicked, onChange should also be called
71
- const b = group.find(Choice).at(1);
72
- const bTarget = b.find("ClickableBehavior");
73
- bTarget.simulate("click");
74
- expect(onChange).toHaveBeenCalledTimes(2);
69
+ // Assert
70
+ expect(checkboxes[0]).toHaveAttribute("aria-invalid", "true");
71
+ expect(checkboxes[1]).toHaveAttribute("aria-invalid", "true");
72
+ expect(checkboxes[2]).toHaveAttribute("aria-invalid", "true");
73
+ });
74
+
75
+ it("checks that aria attributes have been added correctly", () => {
76
+ // Arrange, Act
77
+ render(<TestComponent />);
78
+
79
+ const checkboxes = screen.getAllByRole("checkbox");
80
+
81
+ // Assert
82
+ expect(checkboxes[0]).toHaveAttribute("aria-labelledby", "test-a");
83
+ expect(checkboxes[1]).toHaveAttribute("aria-labelledby", "test-b");
84
+ expect(checkboxes[2]).toHaveAttribute("aria-labelledby", "test-c");
85
+ });
75
86
  });
76
87
 
77
- it("checks that aria attributes have been added correctly", () => {
78
- const a = group.find(Choice).at(0);
79
- const b = group.find(Choice).at(1);
80
- const c = group.find(Choice).at(2);
81
- expect(a.find("input").prop("aria-labelledby")).toEqual("test-a");
82
- expect(b.find("input").prop("aria-labelledby")).toEqual("test-b");
83
- expect(c.find("input").prop("aria-labelledby")).toEqual("test-c");
88
+ describe("flexible props", () => {
89
+ it("should render with a React.Node label", () => {
90
+ // Arrange, Act
91
+ render(
92
+ <CheckboxGroup
93
+ label={
94
+ <span>
95
+ label with <strong>strong</strong> text
96
+ </span>
97
+ }
98
+ groupName="test"
99
+ onChange={() => {}}
100
+ selectedValues={[]}
101
+ >
102
+ <Choice label="a" value="a" aria-labelledby="test-a" />
103
+ <Choice label="b" value="b" aria-labelledby="test-b" />
104
+ <Choice label="c" value="c" aria-labelledby="test-c" />
105
+ </CheckboxGroup>,
106
+ );
107
+
108
+ // Assert
109
+ expect(screen.getByText("strong")).toBeInTheDocument();
110
+ });
111
+
112
+ it("should render with a React.Node description", () => {
113
+ // Arrange, Act
114
+ render(
115
+ <CheckboxGroup
116
+ label="label"
117
+ description={
118
+ <span>
119
+ description with <strong>strong</strong> text
120
+ </span>
121
+ }
122
+ groupName="test"
123
+ onChange={() => {}}
124
+ selectedValues={[]}
125
+ >
126
+ <Choice label="a" value="a" aria-labelledby="test-a" />
127
+ <Choice label="b" value="b" aria-labelledby="test-b" />
128
+ <Choice label="c" value="c" aria-labelledby="test-c" />
129
+ </CheckboxGroup>,
130
+ );
131
+
132
+ // Assert
133
+ expect(screen.getByText("strong")).toBeInTheDocument();
134
+ });
84
135
  });
85
136
  });
@@ -4,6 +4,13 @@ import {mount} from "enzyme";
4
4
  import "jest-enzyme";
5
5
  import {StyleSheet} from "aphrodite";
6
6
 
7
+ import {I18nInlineMarkup} from "@khanacademy/wonder-blocks-i18n";
8
+ import {
9
+ Body,
10
+ LabelMedium,
11
+ LabelSmall,
12
+ } from "@khanacademy/wonder-blocks-typography";
13
+
7
14
  import FieldHeading from "../field-heading.js";
8
15
  import TextField from "../text-field.js";
9
16
 
@@ -180,4 +187,37 @@ describe("FieldHeading", () => {
180
187
  const container = wrapper.find("View").at(0);
181
188
  expect(container).toHaveStyle(styles.style1);
182
189
  });
190
+
191
+ it("should render a LabelSmall when the 'label' prop is a I18nInlineMarkup", () => {
192
+ // Arrange
193
+
194
+ // Act
195
+ const wrapper = mount(
196
+ <FieldHeading
197
+ field={<TextField id="tf-1" value="" onChange={() => {}} />}
198
+ label={<I18nInlineMarkup>Hello, world!</I18nInlineMarkup>}
199
+ />,
200
+ );
201
+
202
+ // Assert
203
+ const label = wrapper.find(LabelMedium);
204
+ expect(label).toExist();
205
+ });
206
+
207
+ it("should render a LabelSmall when the 'description' prop is a I18nInlineMarkup", () => {
208
+ // Arrange
209
+
210
+ // Act
211
+ const wrapper = mount(
212
+ <FieldHeading
213
+ field={<TextField id="tf-1" value="" onChange={() => {}} />}
214
+ label={<Body>Hello, world</Body>}
215
+ description={<I18nInlineMarkup>description</I18nInlineMarkup>}
216
+ />,
217
+ );
218
+
219
+ // Assert
220
+ const label = wrapper.find(LabelSmall);
221
+ expect(label).toExist();
222
+ });
183
223
  });
@@ -1,85 +1,158 @@
1
1
  //@flow
2
2
  import * as React from "react";
3
- import {mount} from "enzyme";
4
- import "jest-enzyme";
3
+ import {render, screen} from "@testing-library/react";
4
+ import userEvent from "@testing-library/user-event";
5
5
 
6
6
  import RadioGroup from "../radio-group.js";
7
7
  import Choice from "../choice.js";
8
8
 
9
9
  describe("RadioGroup", () => {
10
- let group;
11
- const onChange = jest.fn();
12
-
13
- beforeEach(() => {
14
- group = mount(
10
+ const TestComponent = ({
11
+ errorMessage,
12
+ onChange,
13
+ }: {|
14
+ errorMessage?: string,
15
+ onChange?: () => mixed,
16
+ |}) => {
17
+ const [selectedValue, setSelectedValue] = React.useState("a");
18
+ const handleChange = (selectedValue) => {
19
+ setSelectedValue(selectedValue);
20
+ onChange?.();
21
+ };
22
+ return (
15
23
  <RadioGroup
16
24
  label="Test"
17
25
  description="test description"
18
26
  groupName="test"
19
- onChange={onChange}
20
- selectedValue="a"
27
+ onChange={handleChange}
28
+ selectedValue={selectedValue}
29
+ errorMessage={errorMessage}
21
30
  >
22
31
  <Choice label="a" value="a" aria-labelledby="test-a" />
23
32
  <Choice label="b" value="b" aria-labelledby="test-b" />
24
33
  <Choice label="c" value="c" aria-labelledby="test-c" />
25
- </RadioGroup>,
34
+ </RadioGroup>
26
35
  );
27
- });
36
+ };
28
37
 
29
- it("selects only one item at a time", () => {
30
- const a = group.find(Choice).at(0);
31
- const b = group.find(Choice).at(1);
32
- const c = group.find(Choice).at(2);
38
+ describe("behavior", () => {
39
+ it("selects only one item at a time", () => {
40
+ // Arrange, Act
41
+ render(<TestComponent />);
33
42
 
34
- // a starts off checked
35
- expect(a.prop("checked")).toEqual(true);
36
- expect(b.prop("checked")).toEqual(false);
37
- expect(c.prop("checked")).toEqual(false);
38
- });
43
+ const radios = screen.getAllByRole("radio");
39
44
 
40
- it("changes selection when selectedValue changes", () => {
41
- group.setProps({selectedValue: "b"});
42
- const a = group.find(Choice).at(0);
43
- const b = group.find(Choice).at(1);
44
- const c = group.find(Choice).at(2);
45
+ // Assert
46
+ // a starts off checked
47
+ expect(radios[0]).toBeChecked();
48
+ expect(radios[1]).not.toBeChecked();
49
+ expect(radios[2]).not.toBeChecked();
50
+ });
45
51
 
46
- // now b is checked
47
- expect(a.prop("checked")).toEqual(false);
48
- expect(b.prop("checked")).toEqual(true);
49
- expect(c.prop("checked")).toEqual(false);
50
- });
52
+ it("changes selection when selectedValue changes", () => {
53
+ // Arrange
54
+ render(<TestComponent />);
51
55
 
52
- it("displays error state for all Choice children", () => {
53
- group.setProps({errorMessage: "there's an error"});
54
- const a = group.find(Choice).at(0);
55
- const b = group.find(Choice).at(1);
56
- const c = group.find(Choice).at(2);
56
+ const radios = screen.getAllByRole("radio");
57
57
 
58
- expect(a.prop("error")).toEqual(true);
59
- expect(b.prop("error")).toEqual(true);
60
- expect(c.prop("error")).toEqual(true);
61
- });
58
+ // Act
59
+ userEvent.click(radios[1]);
60
+
61
+ // Assert
62
+ // a starts off checked
63
+ expect(radios[0]).not.toBeChecked();
64
+ expect(radios[1]).toBeChecked();
65
+ expect(radios[2]).not.toBeChecked();
66
+ });
67
+
68
+ it("should set aria-invalid on choices when there's an error message", () => {
69
+ // Arrange, Act
70
+ render(<TestComponent errorMessage="there's an error" />);
71
+
72
+ const radios = screen.getAllByRole("radio");
73
+
74
+ // Assert
75
+ expect(radios[0]).toHaveAttribute("aria-invalid", "true");
76
+ expect(radios[1]).toHaveAttribute("aria-invalid", "true");
77
+ expect(radios[2]).toHaveAttribute("aria-invalid", "true");
78
+ });
62
79
 
63
- it("doesn't change when an already selected item is reselected", () => {
64
- // a is already selected, onChange shouldn't be called
65
- const a = group.find(Choice).at(0);
66
- const aTarget = a.find("ClickableBehavior");
67
- aTarget.simulate("click");
68
- expect(onChange).toHaveBeenCalledTimes(0);
69
-
70
- // now b is clicked, onChange should be called
71
- const b = group.find(Choice).at(1);
72
- const bTarget = b.find("ClickableBehavior");
73
- bTarget.simulate("click");
74
- expect(onChange).toHaveBeenCalledTimes(1);
80
+ it("doesn't change when an already selected item is reselected", () => {
81
+ // Arrange
82
+ const handleChange = jest.fn();
83
+ render(<TestComponent onChange={handleChange} />);
84
+
85
+ const radios = screen.getAllByRole("radio");
86
+
87
+ // Act
88
+ // a is already selected, onChange shouldn't be called
89
+ userEvent.click(radios[0]);
90
+
91
+ // Assert
92
+ expect(handleChange).toHaveBeenCalledTimes(0);
93
+ });
94
+
95
+ it("checks that aria attributes have been added correctly", () => {
96
+ // Arrange, Act
97
+ render(<TestComponent />);
98
+
99
+ const radios = screen.getAllByRole("radio");
100
+
101
+ // Assert
102
+ expect(radios[0]).toHaveAttribute("aria-labelledby", "test-a");
103
+ expect(radios[1]).toHaveAttribute("aria-labelledby", "test-b");
104
+ expect(radios[2]).toHaveAttribute("aria-labelledby", "test-c");
105
+ });
75
106
  });
76
107
 
77
- it("checks that aria attributes have been added correctly", () => {
78
- const a = group.find(Choice).at(0);
79
- const b = group.find(Choice).at(1);
80
- const c = group.find(Choice).at(2);
81
- expect(a.find("input").prop("aria-labelledby")).toEqual("test-a");
82
- expect(b.find("input").prop("aria-labelledby")).toEqual("test-b");
83
- expect(c.find("input").prop("aria-labelledby")).toEqual("test-c");
108
+ describe("flexible props", () => {
109
+ it("should render with a React.Node label", () => {
110
+ // Arrange, Act
111
+ const action = () =>
112
+ render(
113
+ <RadioGroup
114
+ label={
115
+ <span>
116
+ label with <strong>strong</strong> text
117
+ </span>
118
+ }
119
+ groupName="test"
120
+ onChange={() => {}}
121
+ selectedValue={"a"}
122
+ >
123
+ <Choice label="a" value="a" aria-labelledby="test-a" />
124
+ <Choice label="b" value="b" aria-labelledby="test-b" />
125
+ <Choice label="c" value="c" aria-labelledby="test-c" />
126
+ </RadioGroup>,
127
+ );
128
+
129
+ // Assert
130
+ expect(action).not.toThrow();
131
+ });
132
+
133
+ it("should render with a React.Node description", () => {
134
+ // Arrange, Act
135
+ const action = () =>
136
+ render(
137
+ <RadioGroup
138
+ label="label"
139
+ description={
140
+ <span>
141
+ description with <strong>strong</strong> text
142
+ </span>
143
+ }
144
+ groupName="test"
145
+ onChange={() => {}}
146
+ selectedValue={"a"}
147
+ >
148
+ <Choice label="a" value="a" aria-labelledby="test-a" />
149
+ <Choice label="b" value="b" aria-labelledby="test-b" />
150
+ <Choice label="c" value="c" aria-labelledby="test-c" />
151
+ </RadioGroup>,
152
+ );
153
+
154
+ // Assert
155
+ expect(action).not.toThrow();
156
+ });
84
157
  });
85
158
  });
@@ -5,11 +5,7 @@ import * as React from "react";
5
5
  import {View, addStyle} from "@khanacademy/wonder-blocks-core";
6
6
  import {Strut} from "@khanacademy/wonder-blocks-layout";
7
7
  import Spacing from "@khanacademy/wonder-blocks-spacing";
8
- import {
9
- type Typography,
10
- LabelMedium,
11
- LabelSmall,
12
- } from "@khanacademy/wonder-blocks-typography";
8
+ import {LabelMedium, LabelSmall} from "@khanacademy/wonder-blocks-typography";
13
9
  import type {StyleType} from "@khanacademy/wonder-blocks-core";
14
10
 
15
11
  import styles from "./group-styles.js";
@@ -32,12 +28,12 @@ type CheckboxGroupProps = {|
32
28
  * Optional label for the group. This label is optional to allow for
33
29
  * greater flexibility in implementing checkbox and radio groups.
34
30
  */
35
- label?: string | React.Element<Typography>,
31
+ label?: React.Node,
36
32
 
37
33
  /**
38
34
  * Optional description for the group.
39
35
  */
40
- description?: string | React.Element<Typography>,
36
+ description?: React.Node,
41
37
 
42
38
  /**
43
39
  * Optional error message. If supplied, the group will be displayed in an
@@ -136,19 +132,15 @@ export default class CheckboxGroup extends React.Component<CheckboxGroupProps> {
136
132
  <StyledFieldset data-test-id={testId} style={styles.fieldset}>
137
133
  {/* We have a View here because fieldset cannot be used with flexbox*/}
138
134
  <View style={style}>
139
- {typeof label === "string" ? (
135
+ {label && (
140
136
  <StyledLegend style={styles.legend}>
141
137
  <LabelMedium>{label}</LabelMedium>
142
138
  </StyledLegend>
143
- ) : (
144
- label && label
145
139
  )}
146
- {typeof description === "string" ? (
140
+ {description && (
147
141
  <LabelSmall style={styles.description}>
148
142
  {description}
149
143
  </LabelSmall>
150
- ) : (
151
- description && description
152
144
  )}
153
145
  {errorMessage && (
154
146
  <LabelSmall style={styles.error}>
@@ -33,12 +33,12 @@ type ChoiceComponentProps = {|
33
33
  /**
34
34
  * Optional label for the field.
35
35
  */
36
- label?: string,
36
+ label?: React.Node,
37
37
 
38
38
  /**
39
39
  * Optional description for the field.
40
40
  */
41
- description?: string,
41
+ description?: React.Node,
42
42
 
43
43
  /**
44
44
  * Unique identifier attached to the HTML input element. If used, need to
@@ -52,10 +52,10 @@ type Props = {|
52
52
  /**
53
53
  * Label for the field.
54
54
  */
55
- label?: string,
55
+ label?: React.Node,
56
56
 
57
57
  /** Optional description for the field. */
58
- description?: string,
58
+ description?: React.Node,
59
59
 
60
60
  /** Auto-populated by parent's groupName prop if in a group. */
61
61
  groupName?: string,
@@ -142,7 +142,9 @@ type DefaultProps = {|
142
142
  return (
143
143
  <UniqueIDProvider mockOnFirstRender={true} scope="choice">
144
144
  {(ids) => {
145
- const descriptionId = description && ids.get("description");
145
+ const descriptionId = description
146
+ ? ids.get("description")
147
+ : undefined;
146
148
 
147
149
  return (
148
150
  <View style={style} className={className}>
@@ -10,10 +10,10 @@ type Props = {|
10
10
  ...AriaProps,
11
11
 
12
12
  /** User-defined. Label for the field. */
13
- label: string,
13
+ label: React.Node,
14
14
 
15
15
  /** User-defined. Optional description for the field. */
16
- description?: string,
16
+ description?: React.Node,
17
17
 
18
18
  /** User-defined. Should be distinct for each item in the group. */
19
19
  value: string,