Skip to content

Commit

Permalink
TextField and TextArea: add error prop (#2347)
Browse files Browse the repository at this point in the history
## 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
  • Loading branch information
beaesguerra authored Nov 13, 2024
1 parent fece121 commit cdcfe1b
Show file tree
Hide file tree
Showing 10 changed files with 399 additions and 52 deletions.
5 changes: 5 additions & 0 deletions .changeset/cold-cows-sit.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@khanacademy/wonder-blocks-form": minor
---

- 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.
4 changes: 1 addition & 3 deletions __docs__/wonder-blocks-form/text-area-variants.stories.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,6 @@ import {TextArea} from "@khanacademy/wonder-blocks-form";
/**
* The following stories are used to generate the pseudo states for the
* TextArea component. This is only used for visual testing in Chromatic.
*
* Note: Error state is not shown on initial render if the TextArea value is empty.
*/
export default {
title: "Packages / Form / TextArea / All Variants",
Expand Down Expand Up @@ -40,7 +38,7 @@ const states = [
},
{
label: "Error",
props: {validate: () => "Error"},
props: {error: true},
},
];
const States = (props: {
Expand Down
129 changes: 122 additions & 7 deletions __docs__/wonder-blocks-form/text-area.stories.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ import {color, spacing} from "@khanacademy/wonder-blocks-tokens";
import Button from "@khanacademy/wonder-blocks-button";
import {LabelSmall, LabelLarge} from "@khanacademy/wonder-blocks-typography";
import {Strut} from "@khanacademy/wonder-blocks-layout";
import {View} from "@khanacademy/wonder-blocks-core";
import {PropsFor, View} from "@khanacademy/wonder-blocks-core";

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

Expand Down Expand Up @@ -60,9 +60,9 @@ const styles = StyleSheet.create({
},
});

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

const handleChange = (newValue: string) => {
setValue(newValue);
Expand All @@ -77,7 +77,11 @@ const ControlledTextArea = (args: any) => {
onValidate={setError}
/>
<Strut size={spacing.xxSmall_6} />
{error && <LabelSmall style={styles.error}>{error}</LabelSmall>}
{(error || args.error) && (
<LabelSmall style={styles.error}>
{error || "Error from error prop"}
</LabelSmall>
)}
</View>
);
};
Expand Down Expand Up @@ -158,14 +162,42 @@ export const ReadOnly: StoryComponentType = {
},
};

/**
* If the `error` prop is set to true, the TextArea will have error styling and
* `aria-invalid` set to `true`.
*
* This is useful for scenarios where we want to show an error on a
* specific field after a form is submitted (server validation).
*
* Note: The `required` and `validate` props can also put the TextArea in an
* error state.
*/
export const Error: StoryComponentType = {
render: ControlledTextArea,
args: {
value: "With error",
error: true,
},
parameters: {
chromatic: {
// Disabling because this doesn't test anything visual.
disableSnapshot: true,
},
},
};

/**
* If the textarea fails validation, `TextArea` will have error styling.
*
* This is useful for scenarios where we want to show errors while a
* user is filling out a form (client validation).
*
* Note that we will internally set the correct `aria-invalid` attribute to the
* `textarea` element:
* - `aria-invalid="true"` if there is an error message.
* - `aria-invalid="false"` if there is no error message.
* - `aria-invalid="true"` if there is an error.
* - `aria-invalid="false"` if there is no error.
*/
export const Error: StoryComponentType = {
export const ErrorFromValidation: StoryComponentType = {
args: {
value: "khan",
validate(value: string) {
Expand All @@ -176,6 +208,89 @@ export const Error: StoryComponentType = {
},
},
render: ControlledTextArea,
parameters: {
chromatic: {
// Disabling because this doesn't test anything visual.
disableSnapshot: true,
},
},
};

/**
* This example shows how the `error` and `validate` props can both be used to
* put the field in an error state. This is useful for scenarios where we want
* to show error while a user is filling out a form (client validation)
* and after a form is submitted (server validation).
*
* In this example:
* 1. It starts with an invalid email. The error message shown is the message returned
* by the `validate` function prop
* 2. Once the email is fixed to `[email protected]`, the validation error message
* goes away since it is a valid email.
* 3. When the Submit button is pressed, another error message is shown (this
* simulates backend validation).
* 4. When you enter any other email address, the error message is
* cleared.
*/
export const ErrorFromPropAndValidation = (args: PropsFor<typeof TextArea>) => {
const [value, setValue] = React.useState(args.value || "test@test,com");
const [validationErrorMessage, setValidationErrorMessage] = React.useState<
string | null | undefined
>(null);
const [backendErrorMessage, setBackendErrorMessage] = React.useState<
string | null | undefined
>(null);

const handleChange = (newValue: string) => {
setValue(newValue);
// Clear the backend error message on change
setBackendErrorMessage(null);
};

const errorMessage = validationErrorMessage || backendErrorMessage;

return (
<View>
<TextArea
{...args}
value={value}
onChange={handleChange}
validate={(value: string) => {
const emailRegex = /^[^@\s]+@[^@\s.]+\.[^@.\s]+$/;
if (!emailRegex.test(value)) {
return "Please enter a valid email";
}
}}
onValidate={setValidationErrorMessage}
error={!!errorMessage}
/>
<Strut size={spacing.xxSmall_6} />
{errorMessage && (
<LabelSmall style={styles.error}>{errorMessage}</LabelSmall>
)}
<Strut size={spacing.xxSmall_6} />
<Button
onClick={() => {
if (value === "[email protected]") {
setBackendErrorMessage(
"This email is already being used, please try another email.",
);
} else {
setBackendErrorMessage(null);
}
}}
>
Submit
</Button>
</View>
);
};

ErrorFromPropAndValidation.parameters = {
chromatic: {
// Disabling because this doesn't test anything visual.
disableSnapshot: true,
},
};

/**
Expand Down
4 changes: 1 addition & 3 deletions __docs__/wonder-blocks-form/text-field-variants.stories.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,6 @@ import {TextField} from "@khanacademy/wonder-blocks-form";
/**
* The following stories are used to generate the pseudo states for the
* TextField component. This is only used for visual testing in Chromatic.
*
* Note: Error state is not shown on initial render if the TextField value is empty.
*/
export default {
title: "Packages / Form / TextField / All Variants",
Expand Down Expand Up @@ -40,7 +38,7 @@ const states = [
},
{
label: "Error",
props: {validate: () => "Error"},
props: {error: true},
},
];
const States = (props: {
Expand Down
15 changes: 14 additions & 1 deletion __docs__/wonder-blocks-form/text-field.argtypes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -163,7 +163,7 @@ export default {

validate: {
description:
"Provide a validation for the input value. Return a string error message or null | void for a valid input.",
"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.",
table: {
type: {
summary: "(value: string) => ?string",
Expand All @@ -174,6 +174,19 @@ export default {
},
},

error: {
description:
"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).",
table: {
type: {
summary: "boolean",
},
},
control: {
type: "boolean",
},
},

/**
* Number-specific props
*/
Expand Down
Loading

0 comments on commit cdcfe1b

Please sign in to comment.