Skip to content

Commit cdcfe1b

Browse files
authored
TextField and TextArea: add error prop (#2347)
## Summary: Adds an `error` prop to TextField and TextArea components so they can be put in an error state declaratively. This is useful for validation that happens after a form submission. Related to the LabeledField work: - By having an error prop, the LabeledField component will set the `error` prop to `true` if it is provided with an error message (will do this in a separate PR for LabeledField work) - The error prop is consistent with other fields such as the SingleSelect, MultiSelect, and Combobox components. I'll be updating SearchField in another PR (with other updates)! - These changes can be released separately from the LabeledField changes since they are incremental changes to existing components Issue: WB-1777 ## Test plan: ### TextField - Setting the `error` prop puts the component in an error state (`aria-invalid` is set to `true` and error styling is applied) (`?path=/story/packages-form-textfield--error`) - Validation continues to put the component in an error state if the value is not valid (`?path=/story/packages-form-textfield--error-from-validation`) - Required prop continues to put the component in an error state if a value is cleared (`?path=/story/packages-form-textfield--required`) ### TextArea - Setting the `error` prop puts the component in an error state (`aria-invalid` is set to `true` and error styling is applied) (`?path=/story/packages-form-textarea--error`) - Validation continues to put the component in an error state if the value is not valid (`?path=/story/packages-form-textarea--error-from-validation`) - Required prop continues to put the component in an error state if a value is cleared (`?path=/story/packages-form-textarea--required`) Author: beaesguerra Reviewers: beaesguerra, jandrade Required Reviewers: Approved By: jandrade Checks: ✅ Chromatic - Get results on regular PRs (ubuntu-latest, 20.x), ✅ Lint / Lint (ubuntu-latest, 20.x), ✅ Test / Test (ubuntu-latest, 20.x, 2/2), ✅ Test / Test (ubuntu-latest, 20.x, 1/2), ✅ Check build sizes (ubuntu-latest, 20.x), ✅ Publish npm snapshot (ubuntu-latest, 20.x), ✅ Check for .changeset entries for all changed files (ubuntu-latest, 20.x), ✅ Chromatic - Build on regular PRs / chromatic (ubuntu-latest, 20.x), ⏭️ Chromatic - Skip on Release PR (changesets), ✅ Prime node_modules cache for primary configuration (ubuntu-latest, 20.x), ✅ gerald, ⏭️ dependabot Pull Request URL: #2347
1 parent fece121 commit cdcfe1b

File tree

10 files changed

+399
-52
lines changed

10 files changed

+399
-52
lines changed

.changeset/cold-cows-sit.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"@khanacademy/wonder-blocks-form": minor
3+
---
4+
5+
- 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.

__docs__/wonder-blocks-form/text-area-variants.stories.tsx

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -10,8 +10,6 @@ import {TextArea} from "@khanacademy/wonder-blocks-form";
1010
/**
1111
* The following stories are used to generate the pseudo states for the
1212
* TextArea component. This is only used for visual testing in Chromatic.
13-
*
14-
* Note: Error state is not shown on initial render if the TextArea value is empty.
1513
*/
1614
export default {
1715
title: "Packages / Form / TextArea / All Variants",
@@ -40,7 +38,7 @@ const states = [
4038
},
4139
{
4240
label: "Error",
43-
props: {validate: () => "Error"},
41+
props: {error: true},
4442
},
4543
];
4644
const States = (props: {

__docs__/wonder-blocks-form/text-area.stories.tsx

Lines changed: 122 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ import {color, spacing} from "@khanacademy/wonder-blocks-tokens";
1010
import Button from "@khanacademy/wonder-blocks-button";
1111
import {LabelSmall, LabelLarge} from "@khanacademy/wonder-blocks-typography";
1212
import {Strut} from "@khanacademy/wonder-blocks-layout";
13-
import {View} from "@khanacademy/wonder-blocks-core";
13+
import {PropsFor, View} from "@khanacademy/wonder-blocks-core";
1414

1515
import TextAreaArgTypes from "./text-area.argtypes";
1616

@@ -60,9 +60,9 @@ const styles = StyleSheet.create({
6060
},
6161
});
6262

63-
const ControlledTextArea = (args: any) => {
63+
const ControlledTextArea = (args: PropsFor<typeof TextArea>) => {
6464
const [value, setValue] = React.useState(args.value || "");
65-
const [error, setError] = React.useState<string | null>(null);
65+
const [error, setError] = React.useState<string | null | undefined>(null);
6666

6767
const handleChange = (newValue: string) => {
6868
setValue(newValue);
@@ -77,7 +77,11 @@ const ControlledTextArea = (args: any) => {
7777
onValidate={setError}
7878
/>
7979
<Strut size={spacing.xxSmall_6} />
80-
{error && <LabelSmall style={styles.error}>{error}</LabelSmall>}
80+
{(error || args.error) && (
81+
<LabelSmall style={styles.error}>
82+
{error || "Error from error prop"}
83+
</LabelSmall>
84+
)}
8185
</View>
8286
);
8387
};
@@ -158,14 +162,42 @@ export const ReadOnly: StoryComponentType = {
158162
},
159163
};
160164

165+
/**
166+
* If the `error` prop is set to true, the TextArea will have error styling and
167+
* `aria-invalid` set to `true`.
168+
*
169+
* This is useful for scenarios where we want to show an error on a
170+
* specific field after a form is submitted (server validation).
171+
*
172+
* Note: The `required` and `validate` props can also put the TextArea in an
173+
* error state.
174+
*/
175+
export const Error: StoryComponentType = {
176+
render: ControlledTextArea,
177+
args: {
178+
value: "With error",
179+
error: true,
180+
},
181+
parameters: {
182+
chromatic: {
183+
// Disabling because this doesn't test anything visual.
184+
disableSnapshot: true,
185+
},
186+
},
187+
};
188+
161189
/**
162190
* If the textarea fails validation, `TextArea` will have error styling.
191+
*
192+
* This is useful for scenarios where we want to show errors while a
193+
* user is filling out a form (client validation).
194+
*
163195
* Note that we will internally set the correct `aria-invalid` attribute to the
164196
* `textarea` element:
165-
* - `aria-invalid="true"` if there is an error message.
166-
* - `aria-invalid="false"` if there is no error message.
197+
* - `aria-invalid="true"` if there is an error.
198+
* - `aria-invalid="false"` if there is no error.
167199
*/
168-
export const Error: StoryComponentType = {
200+
export const ErrorFromValidation: StoryComponentType = {
169201
args: {
170202
value: "khan",
171203
validate(value: string) {
@@ -176,6 +208,89 @@ export const Error: StoryComponentType = {
176208
},
177209
},
178210
render: ControlledTextArea,
211+
parameters: {
212+
chromatic: {
213+
// Disabling because this doesn't test anything visual.
214+
disableSnapshot: true,
215+
},
216+
},
217+
};
218+
219+
/**
220+
* This example shows how the `error` and `validate` props can both be used to
221+
* put the field in an error state. This is useful for scenarios where we want
222+
* to show error while a user is filling out a form (client validation)
223+
* and after a form is submitted (server validation).
224+
*
225+
* In this example:
226+
* 1. It starts with an invalid email. The error message shown is the message returned
227+
* by the `validate` function prop
228+
* 2. Once the email is fixed to `[email protected]`, the validation error message
229+
* goes away since it is a valid email.
230+
* 3. When the Submit button is pressed, another error message is shown (this
231+
* simulates backend validation).
232+
* 4. When you enter any other email address, the error message is
233+
* cleared.
234+
*/
235+
export const ErrorFromPropAndValidation = (args: PropsFor<typeof TextArea>) => {
236+
const [value, setValue] = React.useState(args.value || "test@test,com");
237+
const [validationErrorMessage, setValidationErrorMessage] = React.useState<
238+
string | null | undefined
239+
>(null);
240+
const [backendErrorMessage, setBackendErrorMessage] = React.useState<
241+
string | null | undefined
242+
>(null);
243+
244+
const handleChange = (newValue: string) => {
245+
setValue(newValue);
246+
// Clear the backend error message on change
247+
setBackendErrorMessage(null);
248+
};
249+
250+
const errorMessage = validationErrorMessage || backendErrorMessage;
251+
252+
return (
253+
<View>
254+
<TextArea
255+
{...args}
256+
value={value}
257+
onChange={handleChange}
258+
validate={(value: string) => {
259+
const emailRegex = /^[^@\s]+@[^@\s.]+\.[^@.\s]+$/;
260+
if (!emailRegex.test(value)) {
261+
return "Please enter a valid email";
262+
}
263+
}}
264+
onValidate={setValidationErrorMessage}
265+
error={!!errorMessage}
266+
/>
267+
<Strut size={spacing.xxSmall_6} />
268+
{errorMessage && (
269+
<LabelSmall style={styles.error}>{errorMessage}</LabelSmall>
270+
)}
271+
<Strut size={spacing.xxSmall_6} />
272+
<Button
273+
onClick={() => {
274+
if (value === "[email protected]") {
275+
setBackendErrorMessage(
276+
"This email is already being used, please try another email.",
277+
);
278+
} else {
279+
setBackendErrorMessage(null);
280+
}
281+
}}
282+
>
283+
Submit
284+
</Button>
285+
</View>
286+
);
287+
};
288+
289+
ErrorFromPropAndValidation.parameters = {
290+
chromatic: {
291+
// Disabling because this doesn't test anything visual.
292+
disableSnapshot: true,
293+
},
179294
};
180295

181296
/**

__docs__/wonder-blocks-form/text-field-variants.stories.tsx

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -10,8 +10,6 @@ import {TextField} from "@khanacademy/wonder-blocks-form";
1010
/**
1111
* The following stories are used to generate the pseudo states for the
1212
* TextField component. This is only used for visual testing in Chromatic.
13-
*
14-
* Note: Error state is not shown on initial render if the TextField value is empty.
1513
*/
1614
export default {
1715
title: "Packages / Form / TextField / All Variants",
@@ -40,7 +38,7 @@ const states = [
4038
},
4139
{
4240
label: "Error",
43-
props: {validate: () => "Error"},
41+
props: {error: true},
4442
},
4543
];
4644
const States = (props: {

__docs__/wonder-blocks-form/text-field.argtypes.ts

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -163,7 +163,7 @@ export default {
163163

164164
validate: {
165165
description:
166-
"Provide a validation for the input value. Return a string error message or null | void for a valid input.",
166+
"Provide a validation for the input value. Return a string error message or null | void for a valid input. \n Use this for errors that are shown to the user while they are filling out a form.",
167167
table: {
168168
type: {
169169
summary: "(value: string) => ?string",
@@ -174,6 +174,19 @@ export default {
174174
},
175175
},
176176

177+
error: {
178+
description:
179+
"Whether this field is in an error state. \n Use this for errors that are triggered by something external to the component (example: an error after form submission).",
180+
table: {
181+
type: {
182+
summary: "boolean",
183+
},
184+
},
185+
control: {
186+
type: "boolean",
187+
},
188+
},
189+
177190
/**
178191
* Number-specific props
179192
*/

0 commit comments

Comments
 (0)