@khanacademy/wonder-blocks-form 4.5.1 → 4.6.1

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.
@@ -1,5 +1,5 @@
1
1
  import * as React from "react";
2
- import * as renderer from "react-test-renderer";
2
+ import {render} from "@testing-library/react";
3
3
 
4
4
  import CheckboxCore from "../components/checkbox-core";
5
5
  import RadioCore from "../components/radio-core";
@@ -12,17 +12,16 @@ describe("CheckboxCore", () => {
12
12
  checkedStates.forEach((checked: any) => {
13
13
  test(`type:${state} checked:${String(checked)}`, () => {
14
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();
15
+ const {container} = render(
16
+ <CheckboxCore
17
+ checked={checked}
18
+ disabled={disabled}
19
+ error={state === "error"}
20
+ onClick={() => {}}
21
+ />,
22
+ );
23
+
24
+ expect(container).toMatchSnapshot();
26
25
  });
27
26
  });
28
27
  });
@@ -33,17 +32,16 @@ describe("RadioCore", () => {
33
32
  checkedStates.forEach((checked: any) => {
34
33
  test(`type:${state} checked:${String(checked)}`, () => {
35
34
  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();
35
+ const {container} = render(
36
+ <RadioCore
37
+ checked={checked}
38
+ disabled={disabled}
39
+ error={state === "error"}
40
+ onClick={() => {}}
41
+ />,
42
+ );
43
+
44
+ expect(container).toMatchSnapshot();
47
45
  });
48
46
  });
49
47
  });
@@ -703,4 +703,45 @@ describe("Required LabeledTextField", () => {
703
703
  errorMessage,
704
704
  );
705
705
  });
706
+
707
+ test("displays an error even when onValidate is set", async () => {
708
+ // Arrange
709
+ const errorMessage = "Empty string!";
710
+
711
+ const validate = (value: string): string | null | undefined => {
712
+ if (value === "") {
713
+ return errorMessage;
714
+ }
715
+ };
716
+
717
+ const TextFieldWrapper = () => {
718
+ const [value, setValue] = React.useState("initial");
719
+ return (
720
+ <LabeledTextField
721
+ label="Label"
722
+ value={value}
723
+ onChange={setValue}
724
+ validate={validate}
725
+ onValidate={jest.fn()}
726
+ testId="test-labeled-text-field"
727
+ />
728
+ );
729
+ };
730
+
731
+ render(<TextFieldWrapper />);
732
+
733
+ const textField = await screen.findByTestId(
734
+ "test-labeled-text-field-field",
735
+ );
736
+ textField.focus();
737
+ await userEvent.clear(textField);
738
+
739
+ // Act
740
+ textField.blur();
741
+
742
+ // Assert
743
+ expect(await screen.findByRole("alert")).toHaveTextContent(
744
+ errorMessage,
745
+ );
746
+ });
706
747
  });
@@ -3,22 +3,20 @@ import * as React from "react";
3
3
  import {IDProvider, StyleType} from "@khanacademy/wonder-blocks-core";
4
4
 
5
5
  import FieldHeading from "./field-heading";
6
- import TextField, {TextFieldType} from "./text-field";
6
+ import TextField from "./text-field";
7
+ import type {NumericInputProps} from "./text-field";
8
+ import {OmitConstrained} from "../util/types";
7
9
 
8
10
  type WithForwardRef = {
9
11
  forwardedRef: React.ForwardedRef<HTMLInputElement>;
10
12
  };
11
13
 
12
- type Props = {
14
+ type CommonProps = {
13
15
  /**
14
16
  * An optional unique identifier for the TextField.
15
17
  * If no id is specified, a unique id will be auto-generated.
16
18
  */
17
19
  id?: string;
18
- /**
19
- * Determines the type of input. Defaults to text.
20
- */
21
- type: TextFieldType;
22
20
  /**
23
21
  * Provide a label for the TextField.
24
22
  */
@@ -122,6 +120,15 @@ type Props = {
122
120
  autoComplete?: string;
123
121
  };
124
122
 
123
+ type OtherInputProps = CommonProps & {
124
+ /**
125
+ * Determines the type of input. Defaults to text.
126
+ */
127
+ type: "text" | "password" | "email" | "tel";
128
+ };
129
+
130
+ type FullNumericInputProps = NumericInputProps & CommonProps;
131
+ type Props = OtherInputProps | FullNumericInputProps;
125
132
  type PropsWithForwardRef = Props & WithForwardRef;
126
133
 
127
134
  type DefaultProps = {
@@ -213,6 +220,14 @@ class LabeledTextField extends React.Component<PropsWithForwardRef, State> {
213
220
  autoComplete,
214
221
  forwardedRef,
215
222
  ariaDescribedby,
223
+ // NOTE: We are not using this prop, but we need to remove it from
224
+ // `otherProps` so it doesn't override the `handleValidate` function
225
+ // call. We use `otherProps` due to a limitation in TypeScript where
226
+ // we can't easily extract the props when using a discriminated
227
+ // union.
228
+ onValidate: _,
229
+ // numeric input props
230
+ ...otherProps
216
231
  } = this.props;
217
232
 
218
233
  return (
@@ -247,6 +262,7 @@ class LabeledTextField extends React.Component<PropsWithForwardRef, State> {
247
262
  readOnly={readOnly}
248
263
  autoComplete={autoComplete}
249
264
  ref={forwardedRef}
265
+ {...otherProps}
250
266
  />
251
267
  }
252
268
  label={label}
@@ -260,7 +276,7 @@ class LabeledTextField extends React.Component<PropsWithForwardRef, State> {
260
276
  }
261
277
  }
262
278
 
263
- type ExportProps = Omit<
279
+ type ExportProps = OmitConstrained<
264
280
  JSX.LibraryManagedAttributes<
265
281
  typeof LabeledTextField,
266
282
  React.ComponentProps<typeof LabeledTextField>
@@ -6,6 +6,7 @@ import {addStyle} from "@khanacademy/wonder-blocks-core";
6
6
  import {styles as typographyStyles} from "@khanacademy/wonder-blocks-typography";
7
7
 
8
8
  import type {StyleType, AriaProps} from "@khanacademy/wonder-blocks-core";
9
+ import {OmitConstrained} from "../util/types";
9
10
 
10
11
  export type TextFieldType = "text" | "password" | "email" | "number" | "tel";
11
12
 
@@ -17,15 +18,11 @@ const defaultErrorMessage = "This field is required.";
17
18
 
18
19
  const StyledInput = addStyle("input");
19
20
 
20
- type Props = AriaProps & {
21
+ type CommonProps = AriaProps & {
21
22
  /**
22
23
  * The unique identifier for the input.
23
24
  */
24
25
  id: string;
25
- /**
26
- * Determines the type of input. Defaults to text.
27
- */
28
- type: TextFieldType;
29
26
  /**
30
27
  * The input value.
31
28
  */
@@ -117,6 +114,30 @@ type Props = AriaProps & {
117
114
  autoComplete?: string;
118
115
  };
119
116
 
117
+ type OtherInputProps = CommonProps & {
118
+ type: "text" | "password" | "email" | "tel";
119
+ };
120
+
121
+ // Props that are only available for inputs of type "number".
122
+ export type NumericInputProps = {
123
+ type: "number";
124
+ /**
125
+ * The minimum numeric value for the input.
126
+ */
127
+ min?: number;
128
+ /**
129
+ * The maximum numeric value for the input.
130
+ */
131
+ max?: number;
132
+ /**
133
+ * The numeric value to increment or decrement by.
134
+ * Requires the input to be multiples of this value.
135
+ */
136
+ step?: number;
137
+ };
138
+
139
+ type FullNumericInputProps = CommonProps & NumericInputProps;
140
+ type Props = OtherInputProps | FullNumericInputProps;
120
141
  type PropsWithForwardRef = Props & WithForwardRef;
121
142
 
122
143
  type DefaultProps = {
@@ -341,7 +362,7 @@ const styles = StyleSheet.create({
341
362
  },
342
363
  });
343
364
 
344
- type ExportProps = Omit<
365
+ type ExportProps = OmitConstrained<
345
366
  JSX.LibraryManagedAttributes<
346
367
  typeof TextField,
347
368
  React.ComponentProps<typeof TextField>
package/src/util/types.ts CHANGED
@@ -77,3 +77,9 @@ export type RadioGroupProps = {
77
77
  /** Value of the selected radio item. */
78
78
  selectedValue: string;
79
79
  };
80
+
81
+ // For more information, see:
82
+ // https://github.com/microsoft/TypeScript/wiki/FAQ#add-a-key-constraint-to-omit
83
+ export type OmitConstrained<T, K> = {
84
+ [P in keyof T as Exclude<P, K & keyof any>]: T[P];
85
+ };