@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.
- package/CHANGELOG.md +21 -0
- package/dist/components/labeled-text-field.d.ts +12 -7
- package/dist/components/text-field.d.ts +24 -6
- package/dist/es/index.js +37 -34
- package/dist/index.js +37 -34
- package/dist/util/types.d.ts +3 -0
- package/package.json +6 -6
- package/src/__tests__/__snapshots__/custom-snapshot.test.tsx.snap +165 -785
- package/src/__tests__/custom-snapshot.test.tsx +21 -23
- package/src/components/__tests__/labeled-text-field.test.tsx +41 -0
- package/src/components/labeled-text-field.tsx +23 -7
- package/src/components/text-field.tsx +27 -6
- package/src/util/types.ts +6 -0
- package/tsconfig-build.tsbuildinfo +1 -1
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import * as React from "react";
|
|
2
|
-
import
|
|
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
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
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
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
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
|
|
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
|
|
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 =
|
|
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
|
|
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 =
|
|
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
|
+
};
|