Skip to content

Commit

Permalink
LabeledField: integrate with field components and fixes (#2399)
Browse files Browse the repository at this point in the history
## Summary:
Putting LabeledField work together with field updates and addressing some issues. Fixes include:

TextField and TextArea:
- Set `aria-required` based on `required` prop
- Always clear error state onChange if instantValidation is false so that external error messages set using the `error` prop can still be cleared (This is useful for clearing any backend errors when the input value changes)

LabeledField updates:
- Let `required` prop be a boolean or string so it can be passed down to the field prop
- Conditionally set spacing above the error message depending on if there is an error message
- Set `required`, `error` and `light` props for LabeledField and field component if it is set on either LabeledField or field component
- Rename `error` prop to `errorMessage` (This technically isn't a breaking change since the batch of labeled field work hasn't been released yet, labeled field work is currently in the `feature/labeled-field` branch)

Stories/docs:
- Add docs on best practices for forms and fields
- Add docs around LabeledTextField deprecation in favour of LabeledField + TextField
- Update LabeledField examples and docs. Includes examples for validation and moving focus to the first field that has an error message after form submission


Issue: WB-1783

## Test plan:
1. Review new docs:
- LabeledField (`?path=/docs/packages-labeledfield--docs`)
- LabeledTextField (`/?path=/docs/packages-form-labeledtextfield-deprecated--docs`)
- Form Best Practices (`?path=/docs/packages-form-overview--docs`)

2. Test LabeledField with Field Validation: Confirm validation works as expected
- LabeledField Required story (`?path=/story/packages-labeledfield--required`)
  - Tabbing through fields without entering anything
  - Submitting without filling in fields
  - Inputting/selecting a value and removing it
- LabeledField Validation story (`?path=/story/packages-labeledfield--validation`)
  - Triggering validation errors
  - Clearing validation errors when selected value changes
  - Submitting the form to see an example for backend errors

Note: Did some preliminary testing with LabeledField and new validation features. Different screen reader and browser combinations had different behaviours around announcing errors so I created WB-1842 for more testing + documentation.

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), ✅ 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), ✅ Check for .changeset entries for all changed files (ubuntu-latest, 20.x), ✅ gerald, ⏭️  dependabot

Pull Request URL: #2399
  • Loading branch information
beaesguerra authored Dec 19, 2024
1 parent 5badeed commit d9bc865
Show file tree
Hide file tree
Showing 18 changed files with 791 additions and 195 deletions.
5 changes: 5 additions & 0 deletions .changeset/chilly-mirrors-add.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@khanacademy/wonder-blocks-form": patch
---

TextField and TextArea: Set `aria-required` if it is required
5 changes: 5 additions & 0 deletions .changeset/metal-maps-move.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@khanacademy/wonder-blocks-form": patch
---

TextField and TextArea validation: Always clear error message onChange if instantValidation=false so externally set error state can still be cleared
5 changes: 5 additions & 0 deletions .changeset/slow-otters-crash.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@khanacademy/wonder-blocks-labeled-field": patch
---

Set required, error and light props for LabeledField and field component if it is set on either LabeledField or field component
5 changes: 5 additions & 0 deletions .changeset/smart-grapes-serve.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@khanacademy/wonder-blocks-labeled-field": patch
---

Use `errorMessage` prop instead of `error` prop for consistency (`error` prop is used for boolean props in form field components).
5 changes: 5 additions & 0 deletions .changeset/spicy-rivers-marry.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@khanacademy/wonder-blocks-labeled-field": patch
---

LabeledField: Let `required` prop be a boolean or string so it can be passed down to the field prop
52 changes: 50 additions & 2 deletions __docs__/wonder-blocks-form/_overview_.mdx
Original file line number Diff line number Diff line change
@@ -1,10 +1,58 @@
import {Meta} from "@storybook/blocks";
import {Meta, Story, Canvas} from "@storybook/blocks";
import * as AccessibilityStories from './accessibility.stories';
import * as LabeledFieldStories from '../wonder-blocks-labeled-field/labeled-field.stories';

<Meta
title="Packages / Form / Overview"
/>

# Form

`wonder-blocks-form` provides building blocks form Form components, including
`wonder-blocks-form` provides building blocks for Form components, including
TextField, TextArea, Choice, Checkbox, RadioButton, etc.


## Best Practices

### Form Field Labels

- Use form field components with the `LabeledField` component. `LabeledField`
provides a consistent pattern for things like the label,
description, required indicator, and error message for a field. It will also
provide unique ids automatically and wire up the proper attributes for
accessibility. Use `LabeledField` for the following components:
- `TextField`
- `TextArea`
- `SingleSelect`
- `MultiSelect`
- `SearchField`
- If you are using the `CheckboxGroup` or `RadioGroup` components, use the
`label` prop for an accessible field label. This uses `<fieldset>` and
`<legend>` elements instead of a `<label>` since the label is for a group of
related controls. See [Grouping Controls](https://www.w3.org/WAI/tutorials/forms/grouping/)
for more details!
- For custom implementations of field labels:
- Make sure the id of the form field element is unique to the page
- When using Wonder Blocks typography components for the form field label,
set the `tag` prop to `label` to change the underlying element to render and
use the `htmlFor` prop. Here is an example of a form field label using Wonder
Blocks components `LabelMedium` and `TextArea`:

<Canvas of={AccessibilityStories.FormLabelExample} />

### Error Validation

- For fields like `TextField`, `TextArea`, and `SearchField` prefer setting
`instantValidation=false`. This makes it so validation occurs on blur after a
user is done interacting with a field.
- Avoid disabling form submission buttons. There could be exceptions if the
button is for one field.
- If there are errors after a form is submitted, programatically move the user's
focus to the first field with an error.

Here is an example of validation behaviour in a form. It validates when a
user is done filling out a field and also shows a validation error once
the form is submitted (this simulates a backend validation error). When the form
is submitted, the focus is also moved to the first field with an error.

<Canvas of={LabeledFieldStories.Validation} />
34 changes: 0 additions & 34 deletions __docs__/wonder-blocks-form/accessibility.mdx

This file was deleted.

2 changes: 1 addition & 1 deletion __docs__/wonder-blocks-form/accessibility.stories.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import {TextArea} from "@khanacademy/wonder-blocks-form";
import {spacing} from "@khanacademy/wonder-blocks-tokens";

export default {
title: "Packages / Form / Accessibility",
title: "Packages / Form / Overview", // Named the same as overiew docs to hide it from the sidebar
parameters: {
previewTabs: {
canvas: {
Expand Down
74 changes: 73 additions & 1 deletion __docs__/wonder-blocks-form/labeled-text-field.stories.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,13 +13,18 @@ import packageConfig from "../../packages/wonder-blocks-form/package.json";

import ComponentInfo from "../../.storybook/components/component-info";
import LabeledTextFieldArgTypes from "./labeled-text-field.argtypes";
import {LabeledField} from "@khanacademy/wonder-blocks-labeled-field";
import TextField from "../../packages/wonder-blocks-form/src/components/text-field";

/**
* ** DEPRECATED: Please use LabeledField with TextField instead. See [Migration
* story](#migration%20to%20labeled%20field) for more details. **
*
* A LabeledTextField is an element used to accept a single line of text from
* the user paired with a label, description, and error field elements.
*/
export default {
title: "Packages / Form / LabeledTextField",
title: "Packages / Form / LabeledTextField (Deprecated)",
component: LabeledTextField,
parameters: {
componentSubtitle: (
Expand Down Expand Up @@ -56,6 +61,73 @@ export const Default: StoryComponentType = {
},
};

/**
* Please use the **LabeledField** component with the **TextField** component
* instead of the `LabeledTextField` component.
*
* LabeledField is more flexible since it is decoupled from specific field
* components. It also allows use of everything supported by TextField since it
* is used directly.
*
* Note: Validation is now handled by the specific field component and an error
* message is passed into LabeledField. For TextField validation, it is preferred
* that validation occurs on blur once a user is done interacting with a field.
* This can be done using `instantValidation=false` on `TextField`, see TextField
* validation docs for more details!
*
* This example shows how LabeledTextField functionality can be mapped to
* LabeledField and TextField components.
*/
export const MigrationToLabeledField: StoryComponentType = {
render: function Story(args) {
const [labeledTextFieldValue, setLabeledTextFieldValue] =
React.useState("");
const [textFieldValue, setTextFieldValue] = React.useState("");
const [textFieldErrorMessage, setTextFieldErrorMessage] =
React.useState<string | null | undefined>("");

const description = "Enter text that is at least 5 characters long.";
const placeholder = "Placeholder";
const required = "Custom required message";
const validate = (value: string) => {
if (value.length < 5) {
return "Should be 5 or more characters";
}
};
return (
<View style={{gap: spacing.xxxLarge_64}}>
<LabeledTextField
{...args}
label="Using LabeledTextField"
description={description}
value={labeledTextFieldValue}
onChange={setLabeledTextFieldValue}
required={required}
placeholder={placeholder}
validate={validate}
/>
<LabeledField
label="Using LabeledField with TextField (recommended)"
description={description}
required={required}
errorMessage={textFieldErrorMessage}
field={
<TextField
{...args}
value={textFieldValue}
onChange={setTextFieldValue}
placeholder={placeholder}
validate={validate}
onValidate={setTextFieldErrorMessage}
instantValidation={false}
/>
}
/>
</View>
);
},
};

export const Text: StoryComponentType = () => {
const [value, setValue] = React.useState("Khan");

Expand Down
Loading

0 comments on commit d9bc865

Please sign in to comment.