@khanacademy/wonder-blocks-form 4.0.7 → 4.0.9

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.
@@ -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 clickableStates = ["default", "hovered", "pressed"];
9
8
  const checkedStates = [false, true];
10
9
 
11
10
  describe("CheckboxCore", () => {
12
11
  states.forEach((state: any) => {
13
- clickableStates.forEach((clickableState: any) => {
14
- checkedStates.forEach((checked: any) => {
15
- test(`type:${state} state:${clickableState} checked:${String(
16
- checked,
17
- )}`, () => {
18
- const disabled = state === "disabled";
19
- const tree = renderer
20
- .create(
21
- <CheckboxCore
22
- checked={checked}
23
- disabled={disabled}
24
- error={state === "error"}
25
- hovered={clickableState === "hovered"}
26
- pressed={clickableState === "pressed"}
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
- clickableStates.forEach((clickableState: any) => {
42
- checkedStates.forEach((checked: any) => {
43
- test(`type:${state} state:${clickableState} checked:${String(
44
- checked,
45
- )}`, () => {
46
- const disabled = state === "disabled";
47
- const tree = renderer
48
- .create(
49
- <RadioCore
50
- checked={checked}
51
- disabled={disabled}
52
- error={state === "error"}
53
- hovered={clickableState === "hovered"}
54
- pressed={clickableState === "pressed"}
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.getAttribute("class")).toMatch(/light/i);
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 () => {
@@ -9,13 +9,6 @@ import type {IconAsset} from "@khanacademy/wonder-blocks-icon";
9
9
 
10
10
  import type {ChoiceCoreProps} from "../util/types";
11
11
 
12
- type Props = ChoiceCoreProps & {
13
- hovered: boolean;
14
- focused: boolean;
15
- pressed: boolean;
16
- waiting: boolean;
17
- };
18
-
19
12
  const {blue, red, white, offWhite, offBlack16, offBlack32, offBlack50} = Color;
20
13
 
21
14
  const StyledInput = addStyle("input");
@@ -27,7 +20,7 @@ const checkboxCheck: IconAsset = {
27
20
  /**
28
21
  * The internal stateless ☑️ Checkbox
29
22
  */
30
- export default class CheckboxCore extends React.Component<Props> {
23
+ export default class CheckboxCore extends React.Component<ChoiceCoreProps> {
31
24
  handleChange: () => void = () => {
32
25
  // Empty because change is handled by ClickableBehavior
33
26
  return;
@@ -41,10 +34,6 @@ export default class CheckboxCore extends React.Component<Props> {
41
34
  groupName,
42
35
  id,
43
36
  testId,
44
- hovered,
45
- focused,
46
- pressed,
47
- waiting: _,
48
37
  ...sharedProps
49
38
  } = this.props;
50
39
 
@@ -53,11 +42,7 @@ export default class CheckboxCore extends React.Component<Props> {
53
42
  const defaultStyle = [
54
43
  sharedStyles.inputReset,
55
44
  sharedStyles.default,
56
- stateStyles.default,
57
- !disabled &&
58
- (pressed
59
- ? stateStyles.active
60
- : (hovered || focused) && stateStyles.focus),
45
+ !disabled && stateStyles.default,
61
46
  disabled && sharedStyles.disabled,
62
47
  ];
63
48
 
@@ -165,13 +150,21 @@ const _generateStyles = (checked: boolean, error: boolean) => {
165
150
  default: {
166
151
  backgroundColor: palette.base,
167
152
  borderWidth: 0,
168
- },
169
- focus: {
170
- boxShadow: `0 0 0 1px ${white}, 0 0 0 3px ${palette.base}`,
171
- },
172
- active: {
173
- boxShadow: `0 0 0 1px ${white}, 0 0 0 3px ${palette.active}`,
174
- background: palette.active,
153
+
154
+ // Focus and hover have the same style. Focus style only shows
155
+ // up with keyboard navigation.
156
+ ":focus-visible": {
157
+ boxShadow: `0 0 0 1px ${white}, 0 0 0 3px ${palette.base}`,
158
+ },
159
+
160
+ ":hover": {
161
+ boxShadow: `0 0 0 1px ${white}, 0 0 0 3px ${palette.base}`,
162
+ },
163
+
164
+ ":active": {
165
+ boxShadow: `0 0 0 1px ${white}, 0 0 0 3px ${palette.active}`,
166
+ background: palette.active,
167
+ },
175
168
  },
176
169
  };
177
170
  } else {
@@ -179,16 +172,26 @@ const _generateStyles = (checked: boolean, error: boolean) => {
179
172
  default: {
180
173
  backgroundColor: error ? fadedRed : white,
181
174
  borderColor: error ? red : offBlack50,
182
- },
183
- focus: {
184
- backgroundColor: error ? fadedRed : white,
185
- borderColor: palette.base,
186
- borderWidth: 2,
187
- },
188
- active: {
189
- backgroundColor: palette.faded,
190
- borderColor: error ? activeRed : blue,
191
- borderWidth: 2,
175
+
176
+ // Focus and hover have the same style. Focus style only shows
177
+ // up with keyboard navigation.
178
+ ":focus-visible": {
179
+ backgroundColor: error ? fadedRed : white,
180
+ borderColor: palette.base,
181
+ borderWidth: 2,
182
+ },
183
+
184
+ ":hover": {
185
+ backgroundColor: error ? fadedRed : white,
186
+ borderColor: palette.base,
187
+ borderWidth: 2,
188
+ },
189
+
190
+ ":active": {
191
+ backgroundColor: palette.faded,
192
+ borderColor: error ? activeRed : blue,
193
+ borderWidth: 2,
194
+ },
192
195
  },
193
196
  };
194
197
  }
@@ -59,8 +59,8 @@ type CheckboxGroupProps = {
59
59
  testId?: string;
60
60
  };
61
61
 
62
- const StyledFieldset = addStyle<"fieldset">("fieldset");
63
- const StyledLegend = addStyle<"legend">("legend");
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
@@ -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";
@@ -69,12 +68,6 @@ type DefaultProps = {
69
68
  error: false,
70
69
  };
71
70
 
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
71
  handleClick: () => void = () => {
79
72
  const {checked, onChange, variant} = this.props;
80
73
  // Radio buttons cannot be unchecked
@@ -91,15 +84,13 @@ type DefaultProps = {
91
84
  return CheckboxCore;
92
85
  }
93
86
  }
94
- getLabel(): React.ReactNode {
95
- const {disabled, id, label} = this.props;
87
+ getLabel(id: string): React.ReactNode {
88
+ const {disabled, label} = this.props;
96
89
  return (
97
90
  <LabelMedium
98
91
  style={[styles.label, disabled && styles.disabledLabel]}
99
92
  >
100
- <label htmlFor={id} onClick={this.handleLabelClick}>
101
- {label}
102
- </label>
93
+ <label htmlFor={id}>{label}</label>
103
94
  </LabelMedium>
104
95
  );
105
96
  }
@@ -115,18 +106,30 @@ type DefaultProps = {
115
106
  const {
116
107
  label,
117
108
  description,
109
+ id,
118
110
  // eslint-disable-next-line @typescript-eslint/no-unused-vars
119
111
  onChange,
120
112
  style,
121
113
  className,
114
+ // eslint-disable-next-line @typescript-eslint/no-unused-vars
122
115
  variant,
123
116
  ...coreProps
124
117
  } = this.props;
125
118
  const ChoiceCore = this.getChoiceCoreComponent();
126
- const ClickableBehavior = getClickableBehavior();
119
+
127
120
  return (
128
121
  <UniqueIDProvider mockOnFirstRender={true} scope="choice">
129
122
  {(ids) => {
123
+ // A choice element should always have a unique ID set
124
+ // so that the label can always refer to this element.
125
+ // This guarantees that clicking on the label will
126
+ // always click on the choice as well. If an ID is
127
+ // passed in as a prop, use that one. Otherwise,
128
+ // create a unique ID using the provider.
129
+ const uniqueId = id || ids.get("main");
130
+
131
+ // Create a unique ID for the description section to be
132
+ // used by this element's `aria-describedby`.
130
133
  const descriptionId = description
131
134
  ? ids.get("description")
132
135
  : undefined;
@@ -134,32 +137,22 @@ type DefaultProps = {
134
137
  return (
135
138
  // @ts-expect-error [FEI-5019] - TS2769 - No overload matches this call.
136
139
  <View style={style} className={className}>
137
- <ClickableBehavior
138
- disabled={coreProps.disabled}
139
- onClick={this.handleClick}
140
- role={variant}
140
+ <View
141
+ style={styles.wrapper}
142
+ // We are resetting the tabIndex=0 from handlers
143
+ // because the ChoiceCore component will receive
144
+ // focus on basis of it being an input element.
145
+ tabIndex={-1}
141
146
  >
142
- {(state, childrenProps) => {
143
- return (
144
- <View
145
- style={styles.wrapper}
146
- {...childrenProps}
147
- // We are resetting the tabIndex=0 from handlers
148
- // because the ChoiceCore component will receive
149
- // focus on basis of it being an input element.
150
- tabIndex={-1}
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>
147
+ <ChoiceCore
148
+ {...coreProps}
149
+ id={uniqueId}
150
+ aria-describedby={descriptionId}
151
+ onClick={this.handleClick}
152
+ />
153
+ <Strut size={Spacing.xSmall_8} />
154
+ {label && this.getLabel(uniqueId)}
155
+ </View>
163
156
  {description && this.getDescription(descriptionId)}
164
157
  </View>
165
158
  );
@@ -175,7 +168,6 @@ const styles = StyleSheet.create({
175
168
  outline: "none",
176
169
  },
177
170
  label: {
178
- userSelect: "none",
179
171
  // NOTE: The checkbox/radio button (height 16px) should be center
180
172
  // aligned with the first line of the label. However, LabelMedium has a
181
173
  // declared line height of 20px, so we need to adjust the top to get the
@@ -6,20 +6,13 @@ import {addStyle} from "@khanacademy/wonder-blocks-core";
6
6
 
7
7
  import type {ChoiceCoreProps} from "../util/types";
8
8
 
9
- type Props = ChoiceCoreProps & {
10
- hovered: boolean;
11
- focused: boolean;
12
- pressed: boolean;
13
- waiting: boolean;
14
- };
15
-
16
9
  const {blue, red, white, offWhite, offBlack16, offBlack32, offBlack50} = Color;
17
10
 
18
11
  const StyledInput = addStyle("input");
19
12
 
20
13
  /**
21
14
  * The internal stateless 🔘 Radio button
22
- */ export default class RadioCore extends React.Component<Props> {
15
+ */ export default class RadioCore extends React.Component<ChoiceCoreProps> {
23
16
  handleChange: () => void = () => {
24
17
  // Empty because change is handled by ClickableBehavior
25
18
  return;
@@ -33,21 +26,13 @@ const StyledInput = addStyle("input");
33
26
  groupName,
34
27
  id,
35
28
  testId,
36
- hovered,
37
- focused,
38
- pressed,
39
- waiting: _,
40
29
  ...sharedProps
41
30
  } = this.props;
42
31
  const stateStyles = _generateStyles(checked, error);
43
32
  const defaultStyle = [
44
33
  sharedStyles.inputReset,
45
34
  sharedStyles.default,
46
- stateStyles.default,
47
- !disabled &&
48
- (pressed
49
- ? stateStyles.active
50
- : (hovered || focused) && stateStyles.focus),
35
+ !disabled && stateStyles.default,
51
36
  disabled && sharedStyles.disabled,
52
37
  ];
53
38
  const props = {
@@ -141,13 +126,21 @@ const _generateStyles = (checked: boolean, error: boolean) => {
141
126
  backgroundColor: white,
142
127
  borderColor: palette.base,
143
128
  borderWidth: size / 4,
144
- },
145
- focus: {
146
- boxShadow: `0 0 0 1px ${white}, 0 0 0 3px ${palette.base}`,
147
- },
148
- active: {
149
- boxShadow: `0 0 0 1px ${white}, 0 0 0 3px ${palette.active}`,
150
- borderColor: palette.active,
129
+
130
+ // Focus and hover have the same style. Focus style only shows
131
+ // up with keyboard navigation.
132
+ ":focus-visible": {
133
+ boxShadow: `0 0 0 1px ${white}, 0 0 0 3px ${palette.base}`,
134
+ },
135
+
136
+ ":hover": {
137
+ boxShadow: `0 0 0 1px ${white}, 0 0 0 3px ${palette.base}`,
138
+ },
139
+
140
+ ":active": {
141
+ boxShadow: `0 0 0 1px ${white}, 0 0 0 3px ${palette.active}`,
142
+ borderColor: palette.active,
143
+ },
151
144
  },
152
145
  };
153
146
  } else {
@@ -155,16 +148,26 @@ const _generateStyles = (checked: boolean, error: boolean) => {
155
148
  default: {
156
149
  backgroundColor: error ? fadedRed : white,
157
150
  borderColor: error ? red : offBlack50,
158
- },
159
- focus: {
160
- backgroundColor: error ? fadedRed : white,
161
- borderColor: palette.base,
162
- borderWidth: 2,
163
- },
164
- active: {
165
- backgroundColor: palette.faded,
166
- borderColor: error ? activeRed : blue,
167
- borderWidth: 2,
151
+
152
+ // Focus and hover have the same style. Focus style only shows
153
+ // up with keyboard navigation.
154
+ ":focus-visible": {
155
+ backgroundColor: error ? fadedRed : white,
156
+ borderColor: palette.base,
157
+ borderWidth: 2,
158
+ },
159
+
160
+ ":hover": {
161
+ backgroundColor: error ? fadedRed : white,
162
+ borderColor: palette.base,
163
+ borderWidth: 2,
164
+ },
165
+
166
+ ":active": {
167
+ backgroundColor: palette.faded,
168
+ borderColor: error ? activeRed : blue,
169
+ borderWidth: 2,
170
+ },
168
171
  },
169
172
  };
170
173
  }
@@ -58,8 +58,8 @@ type RadioGroupProps = {
58
58
  testId?: string;
59
59
  };
60
60
 
61
- const StyledFieldset = addStyle<"fieldset">("fieldset");
62
- const StyledLegend = addStyle<"legend">("legend");
61
+ const StyledFieldset = addStyle("fieldset");
62
+ const StyledLegend = addStyle("legend");
63
63
 
64
64
  /**
65
65
  * A radio group allows only single selection. Like CheckboxGroup, this
@@ -1,9 +1,11 @@
1
1
  import * as React from "react";
2
- import {StyleSheet, css} from "aphrodite";
2
+ import {StyleSheet} from "aphrodite";
3
3
 
4
4
  import Color, {mix, fade} from "@khanacademy/wonder-blocks-color";
5
+ import {addStyle} from "@khanacademy/wonder-blocks-core";
5
6
  import Spacing from "@khanacademy/wonder-blocks-spacing";
6
7
  import {styles as typographyStyles} from "@khanacademy/wonder-blocks-typography";
8
+
7
9
  import type {StyleType, AriaProps} from "@khanacademy/wonder-blocks-core";
8
10
 
9
11
  export type TextFieldType = "text" | "password" | "email" | "number" | "tel";
@@ -14,6 +16,8 @@ type WithForwardRef = {
14
16
 
15
17
  const defaultErrorMessage = "This field is required.";
16
18
 
19
+ const StyledInput = addStyle("input");
20
+
17
21
  type Props = AriaProps & {
18
22
  /**
19
23
  * The unique identifier for the input.
@@ -233,12 +237,10 @@ class TextField extends React.Component<PropsWithForwardRef, State> {
233
237
  // Should only include Aria related props
234
238
  ...otherProps
235
239
  } = this.props;
240
+
236
241
  return (
237
- <input
238
- // @ts-expect-error: we shouldn't be passing `style` to `css()`
239
- // here b/c `style` allows nested arrays of styles, but `css()`
240
- // only allows a flat array.
241
- className={css([
242
+ <StyledInput
243
+ style={[
242
244
  styles.input,
243
245
  typographyStyles.LabelMedium,
244
246
  styles.default,
@@ -247,12 +249,15 @@ class TextField extends React.Component<PropsWithForwardRef, State> {
247
249
  ? styles.disabled
248
250
  : this.state.focused
249
251
  ? [styles.focused, light && styles.defaultLight]
250
- : this.state.error && [
252
+ : !!this.state.error && [
251
253
  styles.error,
252
254
  light && styles.errorLight,
253
255
  ],
256
+ // Cast `this.state.error` into boolean since it's being
257
+ // used as a conditional
258
+ !!this.state.error && styles.error,
254
259
  style && style,
255
- ])}
260
+ ]}
256
261
  id={id}
257
262
  type={type}
258
263
  placeholder={placeholder}
package/src/util/types.ts CHANGED
@@ -22,6 +22,8 @@ export type ChoiceCoreProps = AriaProps & {
22
22
  id?: string;
23
23
  /** Optional test ID for e2e testing */
24
24
  testId?: string;
25
+ /** Function that executes when the choice is clicked. */
26
+ onClick: () => void;
25
27
  };
26
28
 
27
29
  // Props for checkbox and radio button