@khanacademy/wonder-blocks-form 4.10.3 → 5.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.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,37 @@
1
1
  # @khanacademy/wonder-blocks-form
2
2
 
3
+ ## 5.0.0
4
+
5
+ ### Major Changes
6
+
7
+ - e6abdd17: Upgrade to React 18
8
+
9
+ ### Patch Changes
10
+
11
+ - Updated dependencies [e6abdd17]
12
+ - @khanacademy/wonder-blocks-core@8.0.0
13
+ - @khanacademy/wonder-blocks-clickable@5.0.0
14
+ - @khanacademy/wonder-blocks-icon@5.0.0
15
+ - @khanacademy/wonder-blocks-layout@3.0.0
16
+ - @khanacademy/wonder-blocks-tokens@3.0.0
17
+ - @khanacademy/wonder-blocks-typography@3.0.0
18
+
19
+ ## 4.11.0
20
+
21
+ ### Minor Changes
22
+
23
+ - 9ed7bd5b: Adds `instantValidation` prop for TextArea
24
+ - 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.
25
+ - 486c6a80: - `TextField`
26
+ - Add `instantValidation` prop
27
+ - No longer calls `validate` prop if the field is disabled during initialization and on change
28
+ - `TextArea`
29
+ - Validate the value during initialization if the field is not disabled
30
+
31
+ ### Patch Changes
32
+
33
+ - 21f6779a: Refactor TextField from class component to function component
34
+
3
35
  ## 4.10.3
4
36
 
5
37
  ### 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: boolean;
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: boolean;
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: "text" | "password" | "email" | "tel";
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 class TextField extends React.Component<PropsWithForwardRef, State> {
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) => React.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, useOnMountEffect } from '@khanacademy/wonder-blocks-core';
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 _excluded$2 = ["id", "type", "value", "name", "disabled", "onKeyDown", "placeholder", "style", "testId", "readOnly", "autoFocus", "autoComplete", "forwardedRef", "light", "onFocus", "onBlur", "onValidate", "validate", "onChange", "required"];
594
- const defaultErrorMessage$1 = "This field is required.";
595
- const StyledInput = addStyle("input");
596
- class TextField extends React.Component {
597
- constructor(props) {
598
- super(props);
599
- this.state = {
600
- error: null
601
- };
602
- this.maybeValidate = newValue => {
603
- const {
604
- validate,
605
- onValidate,
606
- required
607
- } = this.props;
608
- if (validate) {
609
- const maybeError = validate(newValue) || null;
610
- this.setState({
611
- error: maybeError
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
- this.handleChange = event => {
630
- const {
631
- onChange
632
- } = this.props;
633
- const newValue = event.target.value;
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
- this.handleBlur = event => {
646
- const {
647
- onBlur
648
- } = this.props;
649
- if (onBlur) {
650
- onBlur(event);
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
- componentDidMount() {
671
- if (this.props.value !== "") {
672
- this.maybeValidate(this.props.value);
640
+ };
641
+ useOnMountEffect(() => {
642
+ if (value !== "") {
643
+ handleValidation(value);
673
644
  }
674
- }
675
- render() {
676
- const _this$props = this.props,
677
- {
678
- id,
679
- type,
680
- value,
681
- name,
682
- disabled,
683
- onKeyDown,
684
- placeholder,
685
- style,
686
- testId,
687
- readOnly,
688
- autoFocus,
689
- autoComplete,
690
- forwardedRef
691
- } = _this$props,
692
- otherProps = _objectWithoutPropertiesLoose(_this$props, _excluded$2);
693
- return React.createElement(IDProvider, {
694
- id: id,
695
- scope: "text-field"
696
- }, uniqueId => React.createElement(StyledInput, _extends({
697
- style: [this.getStyles(), style],
698
- id: uniqueId,
699
- type: type,
700
- placeholder: placeholder,
701
- value: value,
702
- name: name,
703
- "aria-disabled": disabled,
704
- onChange: this.handleChange,
705
- onKeyDown: disabled ? undefined : onKeyDown,
706
- onFocus: this.handleFocus,
707
- onBlur: this.handleBlur,
708
- "data-testid": testId,
709
- readOnly: readOnly || disabled,
710
- autoFocus: autoFocus,
711
- autoComplete: autoComplete,
712
- ref: forwardedRef,
713
- "aria-invalid": this.state.error ? "true" : "false"
714
- }, otherProps)));
715
- }
716
- }
717
- TextField.defaultProps = {
718
- type: "text",
719
- disabled: false,
720
- light: false
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 [error, setError] = React.useState(null);
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 handleValidation = newValue => {
1075
- if (validate) {
1076
- const error = validate(newValue) || null;
1077
- setError(error);
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, !!error && styles.error];
1098
- const lightStyles = [styles.light, !disabled && styles.lightFocus, disabled && styles.lightDisabled, !!error && styles.lightError];
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: onBlur,
1143
+ onBlur: handleBlur,
1129
1144
  required: !!required
1130
1145
  }, otherProps, {
1131
- "aria-invalid": !!error
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 _excluded$2 = ["id", "type", "value", "name", "disabled", "onKeyDown", "placeholder", "style", "testId", "readOnly", "autoFocus", "autoComplete", "forwardedRef", "light", "onFocus", "onBlur", "onValidate", "validate", "onChange", "required"];
624
- const defaultErrorMessage$1 = "This field is required.";
625
- const StyledInput = wonderBlocksCore.addStyle("input");
626
- class TextField extends React__namespace.Component {
627
- constructor(props) {
628
- super(props);
629
- this.state = {
630
- error: null
631
- };
632
- this.maybeValidate = newValue => {
633
- const {
634
- validate,
635
- onValidate,
636
- required
637
- } = this.props;
638
- if (validate) {
639
- const maybeError = validate(newValue) || null;
640
- this.setState({
641
- error: maybeError
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
- this.handleChange = event => {
660
- const {
661
- onChange
662
- } = this.props;
663
- const newValue = event.target.value;
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
- this.handleBlur = event => {
676
- const {
677
- onBlur
678
- } = this.props;
679
- if (onBlur) {
680
- onBlur(event);
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
- componentDidMount() {
701
- if (this.props.value !== "") {
702
- this.maybeValidate(this.props.value);
670
+ };
671
+ wonderBlocksCore.useOnMountEffect(() => {
672
+ if (value !== "") {
673
+ handleValidation(value);
703
674
  }
704
- }
705
- render() {
706
- const _this$props = this.props,
707
- {
708
- id,
709
- type,
710
- value,
711
- name,
712
- disabled,
713
- onKeyDown,
714
- placeholder,
715
- style,
716
- testId,
717
- readOnly,
718
- autoFocus,
719
- autoComplete,
720
- forwardedRef
721
- } = _this$props,
722
- otherProps = _objectWithoutPropertiesLoose__default["default"](_this$props, _excluded$2);
723
- return React__namespace.createElement(wonderBlocksCore.IDProvider, {
724
- id: id,
725
- scope: "text-field"
726
- }, uniqueId => React__namespace.createElement(StyledInput, _extends__default["default"]({
727
- style: [this.getStyles(), style],
728
- id: uniqueId,
729
- type: type,
730
- placeholder: placeholder,
731
- value: value,
732
- name: name,
733
- "aria-disabled": disabled,
734
- onChange: this.handleChange,
735
- onKeyDown: disabled ? undefined : onKeyDown,
736
- onFocus: this.handleFocus,
737
- onBlur: this.handleBlur,
738
- "data-testid": testId,
739
- readOnly: readOnly || disabled,
740
- autoFocus: autoFocus,
741
- autoComplete: autoComplete,
742
- ref: forwardedRef,
743
- "aria-invalid": this.state.error ? "true" : "false"
744
- }, otherProps)));
745
- }
746
- }
747
- TextField.defaultProps = {
748
- type: "text",
749
- disabled: false,
750
- light: false
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 [error, setError] = React__namespace.useState(null);
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 handleValidation = newValue => {
1105
- if (validate) {
1106
- const error = validate(newValue) || null;
1107
- setError(error);
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, !!error && styles.error];
1128
- const lightStyles = [styles.light, !disabled && styles.lightFocus, disabled && styles.lightDisabled, !!error && styles.lightError];
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: onBlur,
1173
+ onBlur: handleBlur,
1159
1174
  required: !!required
1160
1175
  }, otherProps, {
1161
- "aria-invalid": !!error
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.10.3",
3
+ "version": "5.0.0",
4
4
  "design": "v1",
5
5
  "description": "Form components for Wonder Blocks.",
6
6
  "main": "dist/index.js",
@@ -16,18 +16,18 @@
16
16
  },
17
17
  "dependencies": {
18
18
  "@babel/runtime": "^7.18.6",
19
- "@khanacademy/wonder-blocks-clickable": "^4.2.9",
20
- "@khanacademy/wonder-blocks-core": "^7.0.1",
21
- "@khanacademy/wonder-blocks-icon": "^4.2.0",
22
- "@khanacademy/wonder-blocks-layout": "^2.2.2",
23
- "@khanacademy/wonder-blocks-tokens": "^2.1.0",
24
- "@khanacademy/wonder-blocks-typography": "^2.1.16"
19
+ "@khanacademy/wonder-blocks-clickable": "^5.0.0",
20
+ "@khanacademy/wonder-blocks-core": "^8.0.0",
21
+ "@khanacademy/wonder-blocks-icon": "^5.0.0",
22
+ "@khanacademy/wonder-blocks-layout": "^3.0.0",
23
+ "@khanacademy/wonder-blocks-tokens": "^3.0.0",
24
+ "@khanacademy/wonder-blocks-typography": "^3.0.0"
25
25
  },
26
26
  "peerDependencies": {
27
27
  "aphrodite": "^1.2.5",
28
- "react": "16.14.0"
28
+ "react": "18.2.0"
29
29
  },
30
30
  "devDependencies": {
31
- "@khanacademy/wb-dev-build-settings": "^1.0.1"
31
+ "@khanacademy/wb-dev-build-settings": "^2.0.0"
32
32
  }
33
33
  }