@khanacademy/wonder-blocks-form 4.10.2 → 4.11.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 +23 -0
- package/dist/components/text-area.d.ts +17 -0
- package/dist/components/text-field.d.ts +21 -26
- package/dist/es/index.js +167 -152
- package/dist/hooks/use-field-validation.d.ts +66 -0
- package/dist/index.js +166 -151
- package/package.json +2 -2
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,28 @@
|
|
|
1
1
|
# @khanacademy/wonder-blocks-form
|
|
2
2
|
|
|
3
|
+
## 4.11.0
|
|
4
|
+
|
|
5
|
+
### Minor Changes
|
|
6
|
+
|
|
7
|
+
- 9ed7bd5b: Adds `instantValidation` prop for TextArea
|
|
8
|
+
- cdcfe1ba: - TextArea and TextField: Adds `error` prop so that the components can be put in an error state explicitly. This is useful for backend validation errors after a form has already been submitted.
|
|
9
|
+
- 486c6a80: - `TextField`
|
|
10
|
+
- Add `instantValidation` prop
|
|
11
|
+
- No longer calls `validate` prop if the field is disabled during initialization and on change
|
|
12
|
+
- `TextArea`
|
|
13
|
+
- Validate the value during initialization if the field is not disabled
|
|
14
|
+
|
|
15
|
+
### Patch Changes
|
|
16
|
+
|
|
17
|
+
- 21f6779a: Refactor TextField from class component to function component
|
|
18
|
+
|
|
19
|
+
## 4.10.3
|
|
20
|
+
|
|
21
|
+
### Patch Changes
|
|
22
|
+
|
|
23
|
+
- Updated dependencies [c1110599]
|
|
24
|
+
- @khanacademy/wonder-blocks-icon@4.2.0
|
|
25
|
+
|
|
3
26
|
## 4.10.2
|
|
4
27
|
|
|
5
28
|
### Patch Changes
|
|
@@ -117,12 +117,29 @@ declare const TextArea: React.ForwardRefExoticComponent<Readonly<import("@khanac
|
|
|
117
117
|
/**
|
|
118
118
|
* Provide a validation for the textarea value.
|
|
119
119
|
* Return a string error message or null | void for a valid input.
|
|
120
|
+
*
|
|
121
|
+
* Use this for errors that are shown to the user while they are filling out
|
|
122
|
+
* a form.
|
|
120
123
|
*/
|
|
121
124
|
validate?: ((value: string) => string | null | void) | undefined;
|
|
122
125
|
/**
|
|
123
126
|
* Called right after the textarea is validated.
|
|
124
127
|
*/
|
|
125
128
|
onValidate?: ((errorMessage?: string | null | undefined) => unknown) | undefined;
|
|
129
|
+
/**
|
|
130
|
+
* If true, textarea is validated as the user types (onChange). If false,
|
|
131
|
+
* it is validated when the user's focus moves out of the field (onBlur).
|
|
132
|
+
* It is preferred that instantValidation is set to `false`, however, it
|
|
133
|
+
* defaults to `true` for backwards compatibility with existing implementations.
|
|
134
|
+
*/
|
|
135
|
+
instantValidation?: boolean | undefined;
|
|
136
|
+
/**
|
|
137
|
+
* Whether the textarea is in an error state.
|
|
138
|
+
*
|
|
139
|
+
* Use this for errors that are triggered by something external to the
|
|
140
|
+
* component (example: an error after form submission).
|
|
141
|
+
*/
|
|
142
|
+
error?: boolean | undefined;
|
|
126
143
|
/**
|
|
127
144
|
* Whether this textarea is required to continue, or the error message to
|
|
128
145
|
* render if this textarea is left blank.
|
|
@@ -31,16 +31,26 @@ type CommonProps = AriaProps & {
|
|
|
31
31
|
* This `disabled` prop will also set the `readonly` attribute to prevent
|
|
32
32
|
* typing in the field.
|
|
33
33
|
*/
|
|
34
|
-
disabled
|
|
34
|
+
disabled?: boolean;
|
|
35
35
|
/**
|
|
36
36
|
* Provide a validation for the input value.
|
|
37
37
|
* Return a string error message or null | void for a valid input.
|
|
38
|
+
*
|
|
39
|
+
* Use this for errors that are shown to the user while they are filling out
|
|
40
|
+
* a form.
|
|
38
41
|
*/
|
|
39
42
|
validate?: (value: string) => string | null | void;
|
|
40
43
|
/**
|
|
41
44
|
* Called right after the TextField input is validated.
|
|
42
45
|
*/
|
|
43
46
|
onValidate?: (errorMessage?: string | null | undefined) => unknown;
|
|
47
|
+
/**
|
|
48
|
+
* If true, TextField is validated as the user types (onChange). If false,
|
|
49
|
+
* it is validated when the user's focus moves out of the field (onBlur).
|
|
50
|
+
* It is preferred that instantValidation is set to `false`, however, it
|
|
51
|
+
* defaults to `true` for backwards compatibility with existing implementations.
|
|
52
|
+
*/
|
|
53
|
+
instantValidation?: boolean;
|
|
44
54
|
/**
|
|
45
55
|
* Called when the value has changed.
|
|
46
56
|
*/
|
|
@@ -61,6 +71,13 @@ type CommonProps = AriaProps & {
|
|
|
61
71
|
* Provide hints or examples of what to enter.
|
|
62
72
|
*/
|
|
63
73
|
placeholder?: string;
|
|
74
|
+
/**
|
|
75
|
+
* Whether the input is in an error state.
|
|
76
|
+
*
|
|
77
|
+
* Use this for errors that are triggered by something external to the
|
|
78
|
+
* component (example: an error after form submission).
|
|
79
|
+
*/
|
|
80
|
+
error?: boolean;
|
|
64
81
|
/**
|
|
65
82
|
* Whether this field is required to to continue, or the error message to
|
|
66
83
|
* render if this field is left blank.
|
|
@@ -87,7 +104,7 @@ type CommonProps = AriaProps & {
|
|
|
87
104
|
/**
|
|
88
105
|
* Change the default focus ring color to fit a dark background.
|
|
89
106
|
*/
|
|
90
|
-
light
|
|
107
|
+
light?: boolean;
|
|
91
108
|
/**
|
|
92
109
|
* Custom styles for the input.
|
|
93
110
|
*/
|
|
@@ -110,7 +127,7 @@ type CommonProps = AriaProps & {
|
|
|
110
127
|
autoComplete?: string;
|
|
111
128
|
};
|
|
112
129
|
type OtherInputProps = CommonProps & {
|
|
113
|
-
type
|
|
130
|
+
type?: "text" | "password" | "email" | "tel";
|
|
114
131
|
};
|
|
115
132
|
export type NumericInputProps = {
|
|
116
133
|
type: "number";
|
|
@@ -131,32 +148,10 @@ export type NumericInputProps = {
|
|
|
131
148
|
type FullNumericInputProps = CommonProps & NumericInputProps;
|
|
132
149
|
type Props = OtherInputProps | FullNumericInputProps;
|
|
133
150
|
type PropsWithForwardRef = Props & WithForwardRef;
|
|
134
|
-
type DefaultProps = {
|
|
135
|
-
type: PropsWithForwardRef["type"];
|
|
136
|
-
disabled: PropsWithForwardRef["disabled"];
|
|
137
|
-
light: PropsWithForwardRef["light"];
|
|
138
|
-
};
|
|
139
|
-
type State = {
|
|
140
|
-
/**
|
|
141
|
-
* Displayed when the validation fails.
|
|
142
|
-
*/
|
|
143
|
-
error: string | null | undefined;
|
|
144
|
-
};
|
|
145
151
|
/**
|
|
146
152
|
* A TextField is an element used to accept a single line of text from the user.
|
|
147
153
|
*/
|
|
148
|
-
declare
|
|
149
|
-
static defaultProps: DefaultProps;
|
|
150
|
-
constructor(props: PropsWithForwardRef);
|
|
151
|
-
state: State;
|
|
152
|
-
componentDidMount(): void;
|
|
153
|
-
maybeValidate: (newValue: string) => void;
|
|
154
|
-
handleChange: (event: React.ChangeEvent<HTMLInputElement>) => unknown;
|
|
155
|
-
handleFocus: (event: React.FocusEvent<HTMLInputElement>) => unknown;
|
|
156
|
-
handleBlur: (event: React.FocusEvent<HTMLInputElement>) => unknown;
|
|
157
|
-
getStyles: () => StyleType;
|
|
158
|
-
render(): React.ReactNode;
|
|
159
|
-
}
|
|
154
|
+
declare const TextField: (props: PropsWithForwardRef) => JSX.Element;
|
|
160
155
|
type ExportProps = OmitConstrained<JSX.LibraryManagedAttributes<typeof TextField, React.ComponentProps<typeof TextField>>, "forwardedRef">;
|
|
161
156
|
/**
|
|
162
157
|
* A TextField is an element used to accept a single line of text from the user.
|
package/dist/es/index.js
CHANGED
|
@@ -2,7 +2,7 @@ import _extends from '@babel/runtime/helpers/extends';
|
|
|
2
2
|
import * as React from 'react';
|
|
3
3
|
import _objectWithoutPropertiesLoose from '@babel/runtime/helpers/objectWithoutPropertiesLoose';
|
|
4
4
|
import { StyleSheet } from 'aphrodite';
|
|
5
|
-
import { addStyle, UniqueIDProvider, View, IDProvider, useUniqueIdWithMock
|
|
5
|
+
import { addStyle, UniqueIDProvider, View, useOnMountEffect, IDProvider, useUniqueIdWithMock } from '@khanacademy/wonder-blocks-core';
|
|
6
6
|
import { Strut } from '@khanacademy/wonder-blocks-layout';
|
|
7
7
|
import { spacing, mix, color, border, font } from '@khanacademy/wonder-blocks-tokens';
|
|
8
8
|
import { LabelMedium, LabelSmall, styles as styles$7 } from '@khanacademy/wonder-blocks-typography';
|
|
@@ -590,134 +590,151 @@ const RadioGroup = React.forwardRef(function RadioGroup(props, ref) {
|
|
|
590
590
|
}));
|
|
591
591
|
});
|
|
592
592
|
|
|
593
|
-
const
|
|
594
|
-
const
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
}, () => {
|
|
613
|
-
if (onValidate) {
|
|
614
|
-
onValidate(maybeError);
|
|
615
|
-
}
|
|
616
|
-
});
|
|
617
|
-
} else if (required) {
|
|
618
|
-
const requiredString = typeof required === "string" ? required : defaultErrorMessage$1;
|
|
619
|
-
const maybeError = newValue ? null : requiredString;
|
|
620
|
-
this.setState({
|
|
621
|
-
error: maybeError
|
|
622
|
-
}, () => {
|
|
623
|
-
if (onValidate) {
|
|
624
|
-
onValidate(maybeError);
|
|
625
|
-
}
|
|
626
|
-
});
|
|
593
|
+
const defaultErrorMessage = "This field is required.";
|
|
594
|
+
const useFieldValidation = ({
|
|
595
|
+
value,
|
|
596
|
+
disabled: _disabled = false,
|
|
597
|
+
validate,
|
|
598
|
+
onValidate,
|
|
599
|
+
required: _required = false,
|
|
600
|
+
instantValidation: _instantValidation = true
|
|
601
|
+
}) => {
|
|
602
|
+
const [errorMessage, setErrorMessage] = React.useState(() => validate && value !== "" && !_disabled && validate(value) || null);
|
|
603
|
+
const onChangeValidation = newValue => {
|
|
604
|
+
if (_instantValidation) {
|
|
605
|
+
handleValidation(newValue);
|
|
606
|
+
} else {
|
|
607
|
+
if (errorMessage) {
|
|
608
|
+
setErrorMessage(null);
|
|
609
|
+
if (onValidate) {
|
|
610
|
+
onValidate(null);
|
|
611
|
+
}
|
|
627
612
|
}
|
|
628
|
-
}
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
this.maybeValidate(newValue);
|
|
635
|
-
onChange(newValue);
|
|
636
|
-
};
|
|
637
|
-
this.handleFocus = event => {
|
|
638
|
-
const {
|
|
639
|
-
onFocus
|
|
640
|
-
} = this.props;
|
|
641
|
-
if (onFocus) {
|
|
642
|
-
onFocus(event);
|
|
613
|
+
}
|
|
614
|
+
};
|
|
615
|
+
const onBlurValidation = newValue => {
|
|
616
|
+
if (!_instantValidation) {
|
|
617
|
+
if (newValue || _required) {
|
|
618
|
+
handleValidation(newValue);
|
|
643
619
|
}
|
|
644
|
-
}
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
620
|
+
}
|
|
621
|
+
};
|
|
622
|
+
const handleValidation = newValue => {
|
|
623
|
+
if (_disabled) {
|
|
624
|
+
return;
|
|
625
|
+
}
|
|
626
|
+
if (validate) {
|
|
627
|
+
const error = validate(newValue) || null;
|
|
628
|
+
setErrorMessage(error);
|
|
629
|
+
if (onValidate) {
|
|
630
|
+
onValidate(error);
|
|
631
|
+
}
|
|
632
|
+
} else if (_required) {
|
|
633
|
+
const requiredString = typeof _required === "string" ? _required : defaultErrorMessage;
|
|
634
|
+
const error = newValue ? null : requiredString;
|
|
635
|
+
setErrorMessage(error);
|
|
636
|
+
if (onValidate) {
|
|
637
|
+
onValidate(error);
|
|
651
638
|
}
|
|
652
|
-
};
|
|
653
|
-
this.getStyles = () => {
|
|
654
|
-
const {
|
|
655
|
-
disabled,
|
|
656
|
-
light
|
|
657
|
-
} = this.props;
|
|
658
|
-
const {
|
|
659
|
-
error
|
|
660
|
-
} = this.state;
|
|
661
|
-
const baseStyles = [styles$2.input, styles$7.LabelMedium];
|
|
662
|
-
const defaultStyles = [styles$2.default, !disabled && styles$2.defaultFocus, disabled && styles$2.disabled, !!error && styles$2.error];
|
|
663
|
-
const lightStyles = [styles$2.light, !disabled && styles$2.lightFocus, disabled && styles$2.lightDisabled, !!error && styles$2.lightError];
|
|
664
|
-
return [...baseStyles, ...(light ? lightStyles : defaultStyles)];
|
|
665
|
-
};
|
|
666
|
-
if (props.validate && props.value !== "") {
|
|
667
|
-
this.state.error = props.validate(props.value) || null;
|
|
668
639
|
}
|
|
669
|
-
}
|
|
670
|
-
|
|
671
|
-
if (
|
|
672
|
-
|
|
640
|
+
};
|
|
641
|
+
useOnMountEffect(() => {
|
|
642
|
+
if (value !== "") {
|
|
643
|
+
handleValidation(value);
|
|
673
644
|
}
|
|
674
|
-
}
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
style
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
onFocus
|
|
707
|
-
onBlur
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
645
|
+
});
|
|
646
|
+
return {
|
|
647
|
+
errorMessage,
|
|
648
|
+
onBlurValidation,
|
|
649
|
+
onChangeValidation
|
|
650
|
+
};
|
|
651
|
+
};
|
|
652
|
+
|
|
653
|
+
const _excluded$2 = ["id", "type", "value", "name", "disabled", "light", "error", "validate", "onValidate", "required", "placeholder", "style", "testId", "readOnly", "autoFocus", "autoComplete", "forwardedRef", "instantValidation", "onKeyDown", "onChange", "onFocus", "onBlur"];
|
|
654
|
+
const StyledInput = addStyle("input");
|
|
655
|
+
const TextField = props => {
|
|
656
|
+
const {
|
|
657
|
+
id,
|
|
658
|
+
type = "text",
|
|
659
|
+
value,
|
|
660
|
+
name,
|
|
661
|
+
disabled = false,
|
|
662
|
+
light = false,
|
|
663
|
+
error,
|
|
664
|
+
validate,
|
|
665
|
+
onValidate,
|
|
666
|
+
required,
|
|
667
|
+
placeholder,
|
|
668
|
+
style,
|
|
669
|
+
testId,
|
|
670
|
+
readOnly,
|
|
671
|
+
autoFocus,
|
|
672
|
+
autoComplete,
|
|
673
|
+
forwardedRef,
|
|
674
|
+
instantValidation = true,
|
|
675
|
+
onKeyDown,
|
|
676
|
+
onChange,
|
|
677
|
+
onFocus,
|
|
678
|
+
onBlur
|
|
679
|
+
} = props,
|
|
680
|
+
otherProps = _objectWithoutPropertiesLoose(props, _excluded$2);
|
|
681
|
+
const {
|
|
682
|
+
errorMessage,
|
|
683
|
+
onBlurValidation,
|
|
684
|
+
onChangeValidation
|
|
685
|
+
} = useFieldValidation({
|
|
686
|
+
value,
|
|
687
|
+
required,
|
|
688
|
+
disabled,
|
|
689
|
+
instantValidation,
|
|
690
|
+
validate,
|
|
691
|
+
onValidate
|
|
692
|
+
});
|
|
693
|
+
const hasError = error || !!errorMessage;
|
|
694
|
+
const handleChange = event => {
|
|
695
|
+
const newValue = event.target.value;
|
|
696
|
+
onChangeValidation(newValue);
|
|
697
|
+
onChange(newValue);
|
|
698
|
+
};
|
|
699
|
+
const handleFocus = event => {
|
|
700
|
+
if (onFocus) {
|
|
701
|
+
onFocus(event);
|
|
702
|
+
}
|
|
703
|
+
};
|
|
704
|
+
const handleBlur = event => {
|
|
705
|
+
onBlurValidation(event.target.value);
|
|
706
|
+
if (onBlur) {
|
|
707
|
+
onBlur(event);
|
|
708
|
+
}
|
|
709
|
+
};
|
|
710
|
+
const getStyles = () => {
|
|
711
|
+
const baseStyles = [styles$2.input, styles$7.LabelMedium];
|
|
712
|
+
const defaultStyles = [styles$2.default, !disabled && styles$2.defaultFocus, disabled && styles$2.disabled, hasError && styles$2.error];
|
|
713
|
+
const lightStyles = [styles$2.light, !disabled && styles$2.lightFocus, disabled && styles$2.lightDisabled, hasError && styles$2.lightError];
|
|
714
|
+
return [...baseStyles, ...(light ? lightStyles : defaultStyles)];
|
|
715
|
+
};
|
|
716
|
+
return React.createElement(IDProvider, {
|
|
717
|
+
id: id,
|
|
718
|
+
scope: "text-field"
|
|
719
|
+
}, uniqueId => React.createElement(StyledInput, _extends({
|
|
720
|
+
style: [getStyles(), style],
|
|
721
|
+
id: uniqueId,
|
|
722
|
+
type: type,
|
|
723
|
+
placeholder: placeholder,
|
|
724
|
+
value: value,
|
|
725
|
+
name: name,
|
|
726
|
+
"aria-disabled": disabled,
|
|
727
|
+
onChange: handleChange,
|
|
728
|
+
onKeyDown: disabled ? undefined : onKeyDown,
|
|
729
|
+
onFocus: handleFocus,
|
|
730
|
+
onBlur: handleBlur,
|
|
731
|
+
"data-testid": testId,
|
|
732
|
+
readOnly: readOnly || disabled,
|
|
733
|
+
autoFocus: autoFocus,
|
|
734
|
+
autoComplete: autoComplete,
|
|
735
|
+
ref: forwardedRef,
|
|
736
|
+
"aria-invalid": hasError
|
|
737
|
+
}, otherProps)));
|
|
721
738
|
};
|
|
722
739
|
const styles$2 = StyleSheet.create({
|
|
723
740
|
input: {
|
|
@@ -1028,8 +1045,7 @@ var labeledTextField = React.forwardRef((props, ref) => React.createElement(Labe
|
|
|
1028
1045
|
forwardedRef: ref
|
|
1029
1046
|
})));
|
|
1030
1047
|
|
|
1031
|
-
const _excluded = ["onChange", "value", "placeholder", "disabled", "id", "testId", "style", "readOnly", "autoComplete", "name", "className", "autoFocus", "rows", "spellCheck", "wrap", "minLength", "maxLength", "onClick", "onKeyDown", "onKeyUp", "onFocus", "onBlur", "validate", "onValidate", "required", "resizeType", "light", "rootStyle"];
|
|
1032
|
-
const defaultErrorMessage = "This field is required.";
|
|
1048
|
+
const _excluded = ["onChange", "value", "placeholder", "disabled", "id", "testId", "style", "readOnly", "autoComplete", "name", "className", "autoFocus", "rows", "spellCheck", "wrap", "minLength", "maxLength", "onClick", "onKeyDown", "onKeyUp", "onFocus", "onBlur", "validate", "onValidate", "required", "resizeType", "light", "rootStyle", "error", "instantValidation"];
|
|
1033
1049
|
const StyledTextArea = addStyle("textarea");
|
|
1034
1050
|
const TextArea = React.forwardRef(function TextArea(props, ref) {
|
|
1035
1051
|
const {
|
|
@@ -1060,42 +1076,41 @@ const TextArea = React.forwardRef(function TextArea(props, ref) {
|
|
|
1060
1076
|
required,
|
|
1061
1077
|
resizeType,
|
|
1062
1078
|
light,
|
|
1063
|
-
rootStyle
|
|
1079
|
+
rootStyle,
|
|
1080
|
+
error,
|
|
1081
|
+
instantValidation = true
|
|
1064
1082
|
} = props,
|
|
1065
1083
|
otherProps = _objectWithoutPropertiesLoose(props, _excluded);
|
|
1066
|
-
const
|
|
1084
|
+
const {
|
|
1085
|
+
errorMessage,
|
|
1086
|
+
onBlurValidation,
|
|
1087
|
+
onChangeValidation
|
|
1088
|
+
} = useFieldValidation({
|
|
1089
|
+
value,
|
|
1090
|
+
disabled,
|
|
1091
|
+
validate,
|
|
1092
|
+
onValidate,
|
|
1093
|
+
required,
|
|
1094
|
+
instantValidation
|
|
1095
|
+
});
|
|
1096
|
+
const hasError = error || !!errorMessage;
|
|
1067
1097
|
const ids = useUniqueIdWithMock("text-area");
|
|
1068
1098
|
const uniqueId = id != null ? id : ids.get("id");
|
|
1069
1099
|
const handleChange = event => {
|
|
1070
1100
|
const newValue = event.target.value;
|
|
1101
|
+
onChangeValidation(newValue);
|
|
1071
1102
|
onChange(newValue);
|
|
1072
|
-
handleValidation(newValue);
|
|
1073
1103
|
};
|
|
1074
|
-
const
|
|
1075
|
-
|
|
1076
|
-
|
|
1077
|
-
|
|
1078
|
-
if (onValidate) {
|
|
1079
|
-
onValidate(error);
|
|
1080
|
-
}
|
|
1081
|
-
} else if (required) {
|
|
1082
|
-
const requiredString = typeof required === "string" ? required : defaultErrorMessage;
|
|
1083
|
-
const error = newValue ? null : requiredString;
|
|
1084
|
-
setError(error);
|
|
1085
|
-
if (onValidate) {
|
|
1086
|
-
onValidate(error);
|
|
1087
|
-
}
|
|
1104
|
+
const handleBlur = event => {
|
|
1105
|
+
onBlurValidation(event.target.value);
|
|
1106
|
+
if (onBlur) {
|
|
1107
|
+
onBlur(event);
|
|
1088
1108
|
}
|
|
1089
1109
|
};
|
|
1090
|
-
useOnMountEffect(() => {
|
|
1091
|
-
if (value !== "") {
|
|
1092
|
-
handleValidation(value);
|
|
1093
|
-
}
|
|
1094
|
-
});
|
|
1095
1110
|
const getStyles = () => {
|
|
1096
1111
|
const baseStyles = [styles.textarea, styles$7.LabelMedium, resizeType && resizeStyles[resizeType]];
|
|
1097
|
-
const defaultStyles = [styles.default, !disabled && styles.defaultFocus, disabled && styles.disabled,
|
|
1098
|
-
const lightStyles = [styles.light, !disabled && styles.lightFocus, disabled && styles.lightDisabled,
|
|
1112
|
+
const defaultStyles = [styles.default, !disabled && styles.defaultFocus, disabled && styles.disabled, hasError && styles.error];
|
|
1113
|
+
const lightStyles = [styles.light, !disabled && styles.lightFocus, disabled && styles.lightDisabled, hasError && styles.lightError];
|
|
1099
1114
|
return [...baseStyles, ...(light ? lightStyles : defaultStyles)];
|
|
1100
1115
|
};
|
|
1101
1116
|
return React.createElement(View, {
|
|
@@ -1125,10 +1140,10 @@ const TextArea = React.forwardRef(function TextArea(props, ref) {
|
|
|
1125
1140
|
onKeyDown: disabled ? undefined : onKeyDown,
|
|
1126
1141
|
onKeyUp: disabled ? undefined : onKeyUp,
|
|
1127
1142
|
onFocus: onFocus,
|
|
1128
|
-
onBlur:
|
|
1143
|
+
onBlur: handleBlur,
|
|
1129
1144
|
required: !!required
|
|
1130
1145
|
}, otherProps, {
|
|
1131
|
-
"aria-invalid":
|
|
1146
|
+
"aria-invalid": hasError
|
|
1132
1147
|
})));
|
|
1133
1148
|
});
|
|
1134
1149
|
const VERTICAL_SPACING_PX = 10;
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
type FieldValidationProps = {
|
|
2
|
+
value: string;
|
|
3
|
+
disabled?: boolean;
|
|
4
|
+
validate?: (value: string) => string | null | void;
|
|
5
|
+
onValidate?: (errorMessage?: string | null | undefined) => unknown;
|
|
6
|
+
required?: boolean | string;
|
|
7
|
+
instantValidation?: boolean;
|
|
8
|
+
};
|
|
9
|
+
/**
|
|
10
|
+
* Hook for validation logic for text based fields. Based on the props provided,
|
|
11
|
+
* the hook will:
|
|
12
|
+
* - call the `validate` and `onValidate` props on initialization and mount
|
|
13
|
+
* - provide validation functions for specific events (onChange and onBlur).
|
|
14
|
+
* These functions will call the `validate` and `onValidate` props as needed.
|
|
15
|
+
*
|
|
16
|
+
* @param {FieldValidationProps} props An object with:
|
|
17
|
+
* - `value` - The value of the field.
|
|
18
|
+
* - `disabled` - If the field is disabled.
|
|
19
|
+
* - `required` - Whether the field is required to continue, or the error
|
|
20
|
+
* message if it is left blank.
|
|
21
|
+
* - `instantValidation` - If the field should be validated instantly.
|
|
22
|
+
* - `validate` - Validation for the field.
|
|
23
|
+
* - `onValidate` - Called after the `validate` prop is called.
|
|
24
|
+
* @returns {object} An object with:
|
|
25
|
+
* - `errorMessage` - The error message from validation.
|
|
26
|
+
* - `onBlurValidation` - Validation logic for when a field is blurred.
|
|
27
|
+
* - `onChangeValidation` - Validation logic for when a field changes.
|
|
28
|
+
*
|
|
29
|
+
* @example
|
|
30
|
+
* export const MyComponent = ({
|
|
31
|
+
* value,
|
|
32
|
+
* disabled,
|
|
33
|
+
* validate,
|
|
34
|
+
* onValidate,
|
|
35
|
+
* required,
|
|
36
|
+
* instantValidation,
|
|
37
|
+
* }) => {
|
|
38
|
+
* const {errorMessage, onBlurValidation, onChangeValidation} =
|
|
39
|
+
* useFieldValidation({
|
|
40
|
+
* value,
|
|
41
|
+
* disabled,
|
|
42
|
+
* validate,
|
|
43
|
+
* onValidate,
|
|
44
|
+
* required,
|
|
45
|
+
* instantValidation,
|
|
46
|
+
* });
|
|
47
|
+
* return (
|
|
48
|
+
* <input
|
|
49
|
+
* onBlur={(event) => {
|
|
50
|
+
* onBlurValidation(event.target.value);
|
|
51
|
+
* }}
|
|
52
|
+
* onChange={(event) => {
|
|
53
|
+
* onChangeValidation(event.target.value);
|
|
54
|
+
* }}
|
|
55
|
+
* aria-invalid={!!errorMessage}
|
|
56
|
+
* />
|
|
57
|
+
* );
|
|
58
|
+
* }
|
|
59
|
+
*
|
|
60
|
+
*/
|
|
61
|
+
export declare const useFieldValidation: ({ value, disabled, validate, onValidate, required, instantValidation, }: FieldValidationProps) => {
|
|
62
|
+
errorMessage: string | null;
|
|
63
|
+
onBlurValidation: (newValue: string) => void;
|
|
64
|
+
onChangeValidation: (newValue: string) => void;
|
|
65
|
+
};
|
|
66
|
+
export {};
|
package/dist/index.js
CHANGED
|
@@ -620,134 +620,151 @@ const RadioGroup = React__namespace.forwardRef(function RadioGroup(props, ref) {
|
|
|
620
620
|
}));
|
|
621
621
|
});
|
|
622
622
|
|
|
623
|
-
const
|
|
624
|
-
const
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
}, () => {
|
|
643
|
-
if (onValidate) {
|
|
644
|
-
onValidate(maybeError);
|
|
645
|
-
}
|
|
646
|
-
});
|
|
647
|
-
} else if (required) {
|
|
648
|
-
const requiredString = typeof required === "string" ? required : defaultErrorMessage$1;
|
|
649
|
-
const maybeError = newValue ? null : requiredString;
|
|
650
|
-
this.setState({
|
|
651
|
-
error: maybeError
|
|
652
|
-
}, () => {
|
|
653
|
-
if (onValidate) {
|
|
654
|
-
onValidate(maybeError);
|
|
655
|
-
}
|
|
656
|
-
});
|
|
623
|
+
const defaultErrorMessage = "This field is required.";
|
|
624
|
+
const useFieldValidation = ({
|
|
625
|
+
value,
|
|
626
|
+
disabled: _disabled = false,
|
|
627
|
+
validate,
|
|
628
|
+
onValidate,
|
|
629
|
+
required: _required = false,
|
|
630
|
+
instantValidation: _instantValidation = true
|
|
631
|
+
}) => {
|
|
632
|
+
const [errorMessage, setErrorMessage] = React__namespace.useState(() => validate && value !== "" && !_disabled && validate(value) || null);
|
|
633
|
+
const onChangeValidation = newValue => {
|
|
634
|
+
if (_instantValidation) {
|
|
635
|
+
handleValidation(newValue);
|
|
636
|
+
} else {
|
|
637
|
+
if (errorMessage) {
|
|
638
|
+
setErrorMessage(null);
|
|
639
|
+
if (onValidate) {
|
|
640
|
+
onValidate(null);
|
|
641
|
+
}
|
|
657
642
|
}
|
|
658
|
-
}
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
this.maybeValidate(newValue);
|
|
665
|
-
onChange(newValue);
|
|
666
|
-
};
|
|
667
|
-
this.handleFocus = event => {
|
|
668
|
-
const {
|
|
669
|
-
onFocus
|
|
670
|
-
} = this.props;
|
|
671
|
-
if (onFocus) {
|
|
672
|
-
onFocus(event);
|
|
643
|
+
}
|
|
644
|
+
};
|
|
645
|
+
const onBlurValidation = newValue => {
|
|
646
|
+
if (!_instantValidation) {
|
|
647
|
+
if (newValue || _required) {
|
|
648
|
+
handleValidation(newValue);
|
|
673
649
|
}
|
|
674
|
-
}
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
650
|
+
}
|
|
651
|
+
};
|
|
652
|
+
const handleValidation = newValue => {
|
|
653
|
+
if (_disabled) {
|
|
654
|
+
return;
|
|
655
|
+
}
|
|
656
|
+
if (validate) {
|
|
657
|
+
const error = validate(newValue) || null;
|
|
658
|
+
setErrorMessage(error);
|
|
659
|
+
if (onValidate) {
|
|
660
|
+
onValidate(error);
|
|
661
|
+
}
|
|
662
|
+
} else if (_required) {
|
|
663
|
+
const requiredString = typeof _required === "string" ? _required : defaultErrorMessage;
|
|
664
|
+
const error = newValue ? null : requiredString;
|
|
665
|
+
setErrorMessage(error);
|
|
666
|
+
if (onValidate) {
|
|
667
|
+
onValidate(error);
|
|
681
668
|
}
|
|
682
|
-
};
|
|
683
|
-
this.getStyles = () => {
|
|
684
|
-
const {
|
|
685
|
-
disabled,
|
|
686
|
-
light
|
|
687
|
-
} = this.props;
|
|
688
|
-
const {
|
|
689
|
-
error
|
|
690
|
-
} = this.state;
|
|
691
|
-
const baseStyles = [styles$2.input, wonderBlocksTypography.styles.LabelMedium];
|
|
692
|
-
const defaultStyles = [styles$2.default, !disabled && styles$2.defaultFocus, disabled && styles$2.disabled, !!error && styles$2.error];
|
|
693
|
-
const lightStyles = [styles$2.light, !disabled && styles$2.lightFocus, disabled && styles$2.lightDisabled, !!error && styles$2.lightError];
|
|
694
|
-
return [...baseStyles, ...(light ? lightStyles : defaultStyles)];
|
|
695
|
-
};
|
|
696
|
-
if (props.validate && props.value !== "") {
|
|
697
|
-
this.state.error = props.validate(props.value) || null;
|
|
698
669
|
}
|
|
699
|
-
}
|
|
700
|
-
|
|
701
|
-
if (
|
|
702
|
-
|
|
670
|
+
};
|
|
671
|
+
wonderBlocksCore.useOnMountEffect(() => {
|
|
672
|
+
if (value !== "") {
|
|
673
|
+
handleValidation(value);
|
|
703
674
|
}
|
|
704
|
-
}
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
style
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
onFocus
|
|
737
|
-
onBlur
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
675
|
+
});
|
|
676
|
+
return {
|
|
677
|
+
errorMessage,
|
|
678
|
+
onBlurValidation,
|
|
679
|
+
onChangeValidation
|
|
680
|
+
};
|
|
681
|
+
};
|
|
682
|
+
|
|
683
|
+
const _excluded$2 = ["id", "type", "value", "name", "disabled", "light", "error", "validate", "onValidate", "required", "placeholder", "style", "testId", "readOnly", "autoFocus", "autoComplete", "forwardedRef", "instantValidation", "onKeyDown", "onChange", "onFocus", "onBlur"];
|
|
684
|
+
const StyledInput = wonderBlocksCore.addStyle("input");
|
|
685
|
+
const TextField = props => {
|
|
686
|
+
const {
|
|
687
|
+
id,
|
|
688
|
+
type = "text",
|
|
689
|
+
value,
|
|
690
|
+
name,
|
|
691
|
+
disabled = false,
|
|
692
|
+
light = false,
|
|
693
|
+
error,
|
|
694
|
+
validate,
|
|
695
|
+
onValidate,
|
|
696
|
+
required,
|
|
697
|
+
placeholder,
|
|
698
|
+
style,
|
|
699
|
+
testId,
|
|
700
|
+
readOnly,
|
|
701
|
+
autoFocus,
|
|
702
|
+
autoComplete,
|
|
703
|
+
forwardedRef,
|
|
704
|
+
instantValidation = true,
|
|
705
|
+
onKeyDown,
|
|
706
|
+
onChange,
|
|
707
|
+
onFocus,
|
|
708
|
+
onBlur
|
|
709
|
+
} = props,
|
|
710
|
+
otherProps = _objectWithoutPropertiesLoose__default["default"](props, _excluded$2);
|
|
711
|
+
const {
|
|
712
|
+
errorMessage,
|
|
713
|
+
onBlurValidation,
|
|
714
|
+
onChangeValidation
|
|
715
|
+
} = useFieldValidation({
|
|
716
|
+
value,
|
|
717
|
+
required,
|
|
718
|
+
disabled,
|
|
719
|
+
instantValidation,
|
|
720
|
+
validate,
|
|
721
|
+
onValidate
|
|
722
|
+
});
|
|
723
|
+
const hasError = error || !!errorMessage;
|
|
724
|
+
const handleChange = event => {
|
|
725
|
+
const newValue = event.target.value;
|
|
726
|
+
onChangeValidation(newValue);
|
|
727
|
+
onChange(newValue);
|
|
728
|
+
};
|
|
729
|
+
const handleFocus = event => {
|
|
730
|
+
if (onFocus) {
|
|
731
|
+
onFocus(event);
|
|
732
|
+
}
|
|
733
|
+
};
|
|
734
|
+
const handleBlur = event => {
|
|
735
|
+
onBlurValidation(event.target.value);
|
|
736
|
+
if (onBlur) {
|
|
737
|
+
onBlur(event);
|
|
738
|
+
}
|
|
739
|
+
};
|
|
740
|
+
const getStyles = () => {
|
|
741
|
+
const baseStyles = [styles$2.input, wonderBlocksTypography.styles.LabelMedium];
|
|
742
|
+
const defaultStyles = [styles$2.default, !disabled && styles$2.defaultFocus, disabled && styles$2.disabled, hasError && styles$2.error];
|
|
743
|
+
const lightStyles = [styles$2.light, !disabled && styles$2.lightFocus, disabled && styles$2.lightDisabled, hasError && styles$2.lightError];
|
|
744
|
+
return [...baseStyles, ...(light ? lightStyles : defaultStyles)];
|
|
745
|
+
};
|
|
746
|
+
return React__namespace.createElement(wonderBlocksCore.IDProvider, {
|
|
747
|
+
id: id,
|
|
748
|
+
scope: "text-field"
|
|
749
|
+
}, uniqueId => React__namespace.createElement(StyledInput, _extends__default["default"]({
|
|
750
|
+
style: [getStyles(), style],
|
|
751
|
+
id: uniqueId,
|
|
752
|
+
type: type,
|
|
753
|
+
placeholder: placeholder,
|
|
754
|
+
value: value,
|
|
755
|
+
name: name,
|
|
756
|
+
"aria-disabled": disabled,
|
|
757
|
+
onChange: handleChange,
|
|
758
|
+
onKeyDown: disabled ? undefined : onKeyDown,
|
|
759
|
+
onFocus: handleFocus,
|
|
760
|
+
onBlur: handleBlur,
|
|
761
|
+
"data-testid": testId,
|
|
762
|
+
readOnly: readOnly || disabled,
|
|
763
|
+
autoFocus: autoFocus,
|
|
764
|
+
autoComplete: autoComplete,
|
|
765
|
+
ref: forwardedRef,
|
|
766
|
+
"aria-invalid": hasError
|
|
767
|
+
}, otherProps)));
|
|
751
768
|
};
|
|
752
769
|
const styles$2 = aphrodite.StyleSheet.create({
|
|
753
770
|
input: {
|
|
@@ -1058,8 +1075,7 @@ var labeledTextField = React__namespace.forwardRef((props, ref) => React__namesp
|
|
|
1058
1075
|
forwardedRef: ref
|
|
1059
1076
|
})));
|
|
1060
1077
|
|
|
1061
|
-
const _excluded = ["onChange", "value", "placeholder", "disabled", "id", "testId", "style", "readOnly", "autoComplete", "name", "className", "autoFocus", "rows", "spellCheck", "wrap", "minLength", "maxLength", "onClick", "onKeyDown", "onKeyUp", "onFocus", "onBlur", "validate", "onValidate", "required", "resizeType", "light", "rootStyle"];
|
|
1062
|
-
const defaultErrorMessage = "This field is required.";
|
|
1078
|
+
const _excluded = ["onChange", "value", "placeholder", "disabled", "id", "testId", "style", "readOnly", "autoComplete", "name", "className", "autoFocus", "rows", "spellCheck", "wrap", "minLength", "maxLength", "onClick", "onKeyDown", "onKeyUp", "onFocus", "onBlur", "validate", "onValidate", "required", "resizeType", "light", "rootStyle", "error", "instantValidation"];
|
|
1063
1079
|
const StyledTextArea = wonderBlocksCore.addStyle("textarea");
|
|
1064
1080
|
const TextArea = React__namespace.forwardRef(function TextArea(props, ref) {
|
|
1065
1081
|
const {
|
|
@@ -1090,42 +1106,41 @@ const TextArea = React__namespace.forwardRef(function TextArea(props, ref) {
|
|
|
1090
1106
|
required,
|
|
1091
1107
|
resizeType,
|
|
1092
1108
|
light,
|
|
1093
|
-
rootStyle
|
|
1109
|
+
rootStyle,
|
|
1110
|
+
error,
|
|
1111
|
+
instantValidation = true
|
|
1094
1112
|
} = props,
|
|
1095
1113
|
otherProps = _objectWithoutPropertiesLoose__default["default"](props, _excluded);
|
|
1096
|
-
const
|
|
1114
|
+
const {
|
|
1115
|
+
errorMessage,
|
|
1116
|
+
onBlurValidation,
|
|
1117
|
+
onChangeValidation
|
|
1118
|
+
} = useFieldValidation({
|
|
1119
|
+
value,
|
|
1120
|
+
disabled,
|
|
1121
|
+
validate,
|
|
1122
|
+
onValidate,
|
|
1123
|
+
required,
|
|
1124
|
+
instantValidation
|
|
1125
|
+
});
|
|
1126
|
+
const hasError = error || !!errorMessage;
|
|
1097
1127
|
const ids = wonderBlocksCore.useUniqueIdWithMock("text-area");
|
|
1098
1128
|
const uniqueId = id != null ? id : ids.get("id");
|
|
1099
1129
|
const handleChange = event => {
|
|
1100
1130
|
const newValue = event.target.value;
|
|
1131
|
+
onChangeValidation(newValue);
|
|
1101
1132
|
onChange(newValue);
|
|
1102
|
-
handleValidation(newValue);
|
|
1103
1133
|
};
|
|
1104
|
-
const
|
|
1105
|
-
|
|
1106
|
-
|
|
1107
|
-
|
|
1108
|
-
if (onValidate) {
|
|
1109
|
-
onValidate(error);
|
|
1110
|
-
}
|
|
1111
|
-
} else if (required) {
|
|
1112
|
-
const requiredString = typeof required === "string" ? required : defaultErrorMessage;
|
|
1113
|
-
const error = newValue ? null : requiredString;
|
|
1114
|
-
setError(error);
|
|
1115
|
-
if (onValidate) {
|
|
1116
|
-
onValidate(error);
|
|
1117
|
-
}
|
|
1134
|
+
const handleBlur = event => {
|
|
1135
|
+
onBlurValidation(event.target.value);
|
|
1136
|
+
if (onBlur) {
|
|
1137
|
+
onBlur(event);
|
|
1118
1138
|
}
|
|
1119
1139
|
};
|
|
1120
|
-
wonderBlocksCore.useOnMountEffect(() => {
|
|
1121
|
-
if (value !== "") {
|
|
1122
|
-
handleValidation(value);
|
|
1123
|
-
}
|
|
1124
|
-
});
|
|
1125
1140
|
const getStyles = () => {
|
|
1126
1141
|
const baseStyles = [styles.textarea, wonderBlocksTypography.styles.LabelMedium, resizeType && resizeStyles[resizeType]];
|
|
1127
|
-
const defaultStyles = [styles.default, !disabled && styles.defaultFocus, disabled && styles.disabled,
|
|
1128
|
-
const lightStyles = [styles.light, !disabled && styles.lightFocus, disabled && styles.lightDisabled,
|
|
1142
|
+
const defaultStyles = [styles.default, !disabled && styles.defaultFocus, disabled && styles.disabled, hasError && styles.error];
|
|
1143
|
+
const lightStyles = [styles.light, !disabled && styles.lightFocus, disabled && styles.lightDisabled, hasError && styles.lightError];
|
|
1129
1144
|
return [...baseStyles, ...(light ? lightStyles : defaultStyles)];
|
|
1130
1145
|
};
|
|
1131
1146
|
return React__namespace.createElement(wonderBlocksCore.View, {
|
|
@@ -1155,10 +1170,10 @@ const TextArea = React__namespace.forwardRef(function TextArea(props, ref) {
|
|
|
1155
1170
|
onKeyDown: disabled ? undefined : onKeyDown,
|
|
1156
1171
|
onKeyUp: disabled ? undefined : onKeyUp,
|
|
1157
1172
|
onFocus: onFocus,
|
|
1158
|
-
onBlur:
|
|
1173
|
+
onBlur: handleBlur,
|
|
1159
1174
|
required: !!required
|
|
1160
1175
|
}, otherProps, {
|
|
1161
|
-
"aria-invalid":
|
|
1176
|
+
"aria-invalid": hasError
|
|
1162
1177
|
})));
|
|
1163
1178
|
});
|
|
1164
1179
|
const VERTICAL_SPACING_PX = 10;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@khanacademy/wonder-blocks-form",
|
|
3
|
-
"version": "4.
|
|
3
|
+
"version": "4.11.0",
|
|
4
4
|
"design": "v1",
|
|
5
5
|
"description": "Form components for Wonder Blocks.",
|
|
6
6
|
"main": "dist/index.js",
|
|
@@ -18,7 +18,7 @@
|
|
|
18
18
|
"@babel/runtime": "^7.18.6",
|
|
19
19
|
"@khanacademy/wonder-blocks-clickable": "^4.2.9",
|
|
20
20
|
"@khanacademy/wonder-blocks-core": "^7.0.1",
|
|
21
|
-
"@khanacademy/wonder-blocks-icon": "^4.
|
|
21
|
+
"@khanacademy/wonder-blocks-icon": "^4.2.0",
|
|
22
22
|
"@khanacademy/wonder-blocks-layout": "^2.2.2",
|
|
23
23
|
"@khanacademy/wonder-blocks-tokens": "^2.1.0",
|
|
24
24
|
"@khanacademy/wonder-blocks-typography": "^2.1.16"
|