Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

TextField and TextArea: add error prop #2347

Open
wants to merge 12 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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.
Copy link
Member Author

@beaesguerra beaesguerra Oct 15, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Since we can explicitly put the component in an error state, the variants stories can properly show the error styling in all cases now! Previously, we weren't able to show an empty field with an error state since validation wouldn't be triggered yet in that case.

Screenshot 2024-10-15 at 5 22 58 PM

*/
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
117 changes: 110 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,36 @@ 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,
},
};

/**
* 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 @@ -178,6 +204,83 @@ export const Error: StoryComponentType = {
render: ControlledTextArea,
};

/**
* 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,
},
};

/**
* A required field will have error styling if the field is left blank. To
* observe this, type something into the field, backspace all the way,
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