-
Notifications
You must be signed in to change notification settings - Fork 258
docs: Reformat TextArea Accessibility #4058
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
base: a11y
Are you sure you want to change the base?
Changes from all commits
f892121
9f50642
4dcf4b1
e5abb8c
15b4e8e
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -110,33 +110,207 @@ examples and accessibility guidance. | |
|
|
||
| ## Accessibility | ||
|
|
||
| `TextArea` should be used with [Form Field](/components/inputs/form-field/) to | ||
| ensure proper labeling, error handling, and help text association. See | ||
| [FormField's accessibility documentation](/components/inputs/form-field/#accessibility) | ||
| for comprehensive guidance on form accessibility best practices. | ||
| The primary accessibility goal for `TextArea` is to give every user a visible, persistent label and | ||
| clear instructions, and to ensure assistive technology users can identify the multi-line field and | ||
| hear hints, errors, required state, and character-limit information when the control receives focus. | ||
|
|
||
| ### Minimum accessible structure | ||
|
|
||
| Match the Basic example: label first, then the input inside `FormField.Field`. This order matches the | ||
| DOM reading sequence and ensures the label's `htmlFor` targets the `<textarea>` before hint text | ||
| follows the control. | ||
|
|
||
| ```tsx | ||
| import {FormField} from '@workday/canvas-kit-react/form-field'; | ||
| import {TextArea} from '@workday/canvas-kit-react/text-area'; | ||
|
|
||
| <FormField> | ||
| <FormField.Label>Leave a Review</FormField.Label> | ||
| <FormField.Field> | ||
| <FormField.Input as={TextArea} onChange={handleChange} value={value} /> | ||
| </FormField.Field> | ||
| </FormField>; | ||
| ``` | ||
|
|
||
| Every `TextArea` requires **`FormField`**, a visible **`FormField.Label`**, and | ||
| **`FormField.Input as={TextArea}`** so the control has a programmatically determinable name, | ||
| relationships, and instructions. See | ||
| [FormField's accessibility documentation](/components/inputs/form-field/#accessibility) for shared | ||
| form-field guidance. Include **`FormField.Hint`** for instructions, validation messages, or character | ||
| counts—`FormField` associates that text with the text area through `aria-describedby` (see | ||
| **Built-in Behaviors**). | ||
|
|
||
| ### Built-in Behaviors | ||
|
|
||
| Canvas Kit applies these automatically when you compose `TextArea` with `FormField` subcomponents. | ||
| **Do not duplicate them** in consuming code. | ||
|
|
||
| **ARIA and DOM** (_applied by subcomponents_): | ||
|
|
||
| - **`TextArea`**: Renders a native `<textarea>` element. Screen readers identify it as a multi-line | ||
| text input. | ||
| - **`FormField.Label`**: `<label>` with `htmlFor` matching the `id` on `FormField.Input` | ||
| (`input-{id}`). Clicking the label moves focus to the text area. | ||
| - **`FormField.Input`**: Applies `useFormFieldInput` to wire `id` (`input-{id}`), `aria-labelledby` | ||
| (`label-{id}`), `aria-describedby` (`hint-{id}` when an `id` exists), `required` when `isRequired`, | ||
| `aria-invalid="true"` when `error="error"`, and passes `error` for visual styling. Caution | ||
| (`error="caution"`) does **not** set `aria-invalid`. | ||
| - **`FormField.Hint`**: `id="hint-{id}"` for description association. | ||
| - Generated IDs: `label-{id}`, `input-{id}`, and `hint-{id}` from the `FormField` `id` (auto-generated | ||
| or custom via the `id` prop). | ||
| - **`TextArea` `disabled`**: Maps to the native `disabled` attribute; disabled fields are removed from | ||
| the tab order. | ||
| - **User-resizable dimensions**: Defaults to `resize: both` so users can adjust the control for visual | ||
| comfort. | ||
|
|
||
| **Implementation notes**: | ||
|
|
||
| - **`FormField.Input` always sets `aria-describedby="hint-{id}"`** when an `id` exists, even if | ||
| `FormField.Hint` is not rendered. Always render `FormField.Hint` when there is hint or error text. | ||
| Omitting `FormField.Hint` leaves a dangling `aria-describedby` reference. | ||
| - **`placeholder`** is visual hint text only. It is not a substitute for `FormField.Label` and | ||
| disappears while typing, which can disorient users with cognitive disabilities. | ||
| - Set **`error`** on **`FormField`**, not on `TextArea` directly, so `aria-invalid` and error styling | ||
| stay in sync. | ||
|
|
||
| **Keyboard** (_standard multi-line text control behavior_): | ||
|
|
||
| - <kbd>Tab</kbd> / <kbd>Shift</kbd> + <kbd>Tab</kbd>: Move focus to and from the text area (native tab | ||
| order). | ||
| - Clicking **`FormField.Label`**: Moves focus to the associated `<textarea>` (native `<label>` behavior). | ||
| - Inside the text area: arrow keys, <kbd>Home</kbd>, <kbd>End</kbd>, and standard text-editing shortcuts | ||
| behave as the browser and operating system provide for `<textarea>` elements. | ||
|
|
||
| **Screen reader expectations** (_when built-in behaviors are used as intended_): | ||
|
|
||
| - On focus, assistive technology announces the field label and, when applicable: required state, | ||
| invalid state (`error="error"`), and hint or error text via `aria-describedby`. | ||
| - The current value or "blank" is announced when the text area receives focus. | ||
| - The Caution state is visual only — `aria-invalid` is **not** set for `error="caution"`. | ||
| - Disabled text areas may be announced as unavailable and are skipped in the tab order. | ||
|
|
||
| For a simple text area field, the DOM looks like: | ||
|
|
||
| ```html | ||
| <div> | ||
| <label id="label-abc" for="input-abc">Leave a Review</label> | ||
| <textarea id="input-abc" aria-labelledby="label-abc"></textarea> | ||
| </div> | ||
| ``` | ||
|
|
||
| When hint text is present: | ||
|
|
||
| ```html | ||
| <div> | ||
| <label id="label-abc" for="input-abc">Comments</label> | ||
| <textarea id="input-abc" aria-labelledby="label-abc" aria-describedby="hint-abc"></textarea> | ||
| <p id="hint-abc">Share any additional feedback.</p> | ||
| </div> | ||
| ``` | ||
|
|
||
| For a field in an error state: | ||
|
|
||
| ```html | ||
| <div> | ||
| <label id="label-abc" for="input-abc">Comments</label> | ||
| <textarea | ||
| id="input-abc" | ||
| aria-labelledby="label-abc" | ||
| aria-describedby="hint-abc" | ||
| aria-invalid="true" | ||
| ></textarea> | ||
| <p id="hint-abc">Error: Please enter at least 10 characters.</p> | ||
| </div> | ||
| ``` | ||
|
|
||
| ### Character Limits | ||
| ### Accessibility Requirements | ||
|
|
||
| When limiting text area length: | ||
| Required in application code for an accessible `TextArea`. Rows marked _(conditional)_ apply only when | ||
| the situation matches—otherwise omit. | ||
|
|
||
| - Use the `maxLength` attribute to enforce the limit programmatically. | ||
| - For longer limits (100+ characters), consider adding character count information to | ||
| `FormField.Hint`. | ||
| - Avoid announcing character counts after every keystroke, as this disrupts screen reader users. | ||
| Check out | ||
| [Debouncing an AriaLiveRegion: TextArea with character limit](https://workday.github.io/canvas-kit/?path=/docs/guides-accessibility-aria-live-regions--docs#debouncing-an-arialiveregion-textarea-with-character-limit) | ||
| for an example of how to wait for users to stop typing before announcing the character count to | ||
| screen readers. | ||
| **If no design spec is provided:** use a visible `FormField.Label`, wrap the control with | ||
| `FormField.Input as={TextArea}`, omit `isHidden`, keep default `resize: both`, and omit a custom `id` | ||
| unless testing or composition requires it. | ||
|
|
||
| ### Screen Reader Experience | ||
| **Character limit with debounced live region** _(conditional — when `maxLength` is set)_: | ||
|
|
||
| When properly implemented with `FormField`, screen readers will announce: | ||
| ```tsx | ||
| import {AccessibleHide, AriaLiveRegion} from '@workday/canvas-kit-react/common'; | ||
| import {FormField} from '@workday/canvas-kit-react/form-field'; | ||
| import {TextArea} from '@workday/canvas-kit-react/text-area'; | ||
|
|
||
| const MAX_CHARACTERS = 200; | ||
|
|
||
| <FormField> | ||
| <FormField.Label>Comments</FormField.Label> | ||
| <FormField.Field> | ||
| <FormField.Input as={TextArea} maxLength={MAX_CHARACTERS} onChange={handleChange} value={value} /> | ||
| <FormField.Hint>{`${value.length} of ${MAX_CHARACTERS} characters`}</FormField.Hint> | ||
| <AriaLiveRegion as={AccessibleHide}>{liveUpdateStr}</AriaLiveRegion> | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Incomplete codegen example: The character-limit snippet references |
||
| </FormField.Field> | ||
| </FormField>; | ||
| ``` | ||
|
|
||
| Debounce live-region updates (for example, 2 seconds after typing stops). Announce immediately when | ||
| the limit is reached. Clear the live region on blur. Do not announce character counts after every | ||
| keystroke. See | ||
| [Debouncing an AriaLiveRegion: TextArea with character limit](?path=/docs/guides-accessibility-aria-live-regions--docs#debouncing-an-arialiveregion-textarea-with-character-limit) | ||
| for a full example. | ||
|
|
||
| **Ref forwarding** _(conditional — when programmatic focus is needed)_: | ||
|
|
||
| ```tsx | ||
| const ref = React.useRef<HTMLTextAreaElement>(null); | ||
|
|
||
| <FormField.Input as={TextArea} ref={ref} onChange={handleChange} value={value} />; | ||
| ``` | ||
|
|
||
| - The label text when the text area receives focus. | ||
| - Required, disabled, or read-only status. | ||
| - Help text and error messages (via `aria-describedby`). | ||
| - The current value or "blank" if empty. | ||
| - That it's a multi-line text input field. | ||
| | Requirement | How to satisfy | | ||
| | --- | --- | | ||
| | Visible label | **`FormField.Label`** with concise, meaningful text | | ||
| | Input wiring | **`FormField.Input as={TextArea}`** wrapping every `TextArea` instance | | ||
| | Hint or error text _(conditional)_ | **`FormField.Hint`** when there is help or validation copy—required because `aria-describedby` is wired by default (see **Built-in Behaviors**) | | ||
| | Error state _(conditional)_ | `error="error"` on **`FormField`** and **`FormField.Hint`** prefixed with "Error:" so users who cannot perceive color can distinguish errors from hints | | ||
| | Caution state _(conditional)_ | `error="caution"` on **`FormField`** with descriptive **`FormField.Hint`** (no `aria-invalid`) | | ||
| | Required field _(conditional)_ | `isRequired` on **`FormField`**; consider a form-level note that asterisk (\*) marks required fields | | ||
| | Disabled field _(conditional)_ | `disabled` on **`FormField.Input`** (passed through to `<textarea>`) | | ||
| | Character limit _(conditional)_ | `maxLength` on **`FormField.Input`**, visible count in **`FormField.Hint`**, and debounced **`AriaLiveRegion`** for screen reader updates | | ||
| | Placeholder _(conditional)_ | Short format example on **`FormField.Input`** only—never as the sole label | | ||
| | Stable IDs _(conditional)_ | `id` prop on **`FormField`** when predictable `label-`, `input-`, and `hint-` IDs are needed for testing | | ||
| | Resize constraints _(conditional)_ | `resize` prop on **`TextArea`**; default to `Both` unless layout requires otherwise | | ||
|
|
||
| **Summary for code generation:** | ||
|
|
||
| - **REQUIRED:** visible label, `FormField.Input as={TextArea}` wiring | ||
| - **CONDITIONAL:** hint/error text, required state, disabled state, character limit with live region, placeholder, stable `id`, resize constraints | ||
|
|
||
| ### Anti-Patterns | ||
|
|
||
| Do **not** generate code that does the following (see **Accessibility Requirements** above for what | ||
| to supply instead): | ||
|
|
||
| - Manually set `id`, `htmlFor`, `aria-labelledby`, `aria-describedby`, `aria-invalid`, or `required` on | ||
| **`FormField.Input`** or **`TextArea`** — `FormField` subcomponents wire these automatically via | ||
| `useFormFieldInput` and related hooks. | ||
| - **Unlabeled text areas**: Do not use `TextArea` without `FormField` and `FormField.Label` (see | ||
| **Minimum accessible structure**). | ||
| - **Placeholder-only labels**: Do not rely on `placeholder` instead of `FormField.Label`; placeholders | ||
| disappear while typing and are poor substitutes for labels. | ||
| - **Single-line input for multi-line content**: Do not use `TextInput` when the user needs to enter | ||
| paragraphs or multi-line text; use `TextArea` instead. | ||
| - **Color-only errors**: Do not set `error="error"` without descriptive text in `FormField.Hint` | ||
| prefixed with `"Error:"` so users who cannot perceive color can distinguish errors from hints (see | ||
| **Error state** in Accessibility Requirements). | ||
| - **Caution for invalid values**: Do not use `error="caution"` when a value is invalid; use | ||
| `error="error"` so `aria-invalid` is exposed. | ||
| - **Broken ID references**: Do not point `aria-describedby` at missing elements; always render | ||
| `FormField.Hint` when hint or error text exists. | ||
| - **Per-keystroke character announcements**: Do not announce character counts after every keystroke; | ||
| debounce `AriaLiveRegion` updates so screen reader users are not interrupted while typing. | ||
| - **Disabling resize unnecessarily**: Do not set `resize` to `none` unless there is a strong design or | ||
| layout requirement; users lose a visual comfort affordance that supports low-vision and motor needs. | ||
| - **Standalone `TextArea` without `FormField`**: Do not render `<TextArea />` outside `FormField.Input`; | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Redundant anti-patterns: "Unlabeled text areas" (line 295) and "Standalone |
||
| the control will lack a programmatically determinable name and description relationships. | ||
|
|
||
| ## Component API | ||
|
|
||
|
|
||
Uh oh!
There was an error while loading. Please reload this page.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Missing component-choice guidance: Per the a11y-docs template, add 1–2 sentences in the intro on when to use
TextAreavs TextInput (multi-line vs single-line).