init
This commit is contained in:
@@ -0,0 +1,426 @@
|
||||
# Handling states, validation, and accessibility
|
||||
|
||||
## Role
|
||||
|
||||
You are implementing how the application responds to user actions and ensuring page-level accessibility. These patterns determine whether users trust the application. Follow them for every form, API call, and user action.
|
||||
|
||||
Aura components handle many accessibility concerns automatically; you are responsible for composition, copy, focus, and page structure. Aura's focus system uses `shadow-focus-ring` (custom shadow token). Never remove or override it.
|
||||
|
||||
All UI elements use Aura components and tokens. Error states use the destructive token family, warnings use warning tokens, success uses success tokens.
|
||||
|
||||
For message wording patterns, see `writing-copy.md`.
|
||||
|
||||
For all Storybook URLs, see `./storybook-links.md`.
|
||||
|
||||
<aura-coverage priority="high">
|
||||
What Aura components handle automatically:
|
||||
|
||||
| Concern | Aura handles | You verify |
|
||||
|---------|-------------|-----------|
|
||||
| Focus indicators | shadow-focus-ring on interactive elements | Not hidden by overflow or z-index |
|
||||
| Keyboard activation | Button: Enter/Space. Input: standard keys | Custom elements also respond |
|
||||
| ARIA roles | Correct roles on Dialog, Segmented Control (Tabs ARIA pattern), etc. | Custom components have roles |
|
||||
| Color contrast | Token pairs designed for AA compliance | Page backgrounds don't reduce contrast |
|
||||
| Dark mode | Semantic tokens adapt automatically | Custom colors also work in dark mode |
|
||||
| Disabled states | Communicated via aria-disabled | Reason for disabled is accessible |
|
||||
| Focus trapping | Dialog traps focus when open | You return focus to trigger on close |
|
||||
</aura-coverage>
|
||||
|
||||
<patterns>
|
||||
|
||||
<pattern name="form-validation" priority="critical">
|
||||
<instruction>
|
||||
- Validate on blur, not on every keystroke
|
||||
- Show errors inline, adjacent to the field
|
||||
- Preserve user input on failure (never clear the form)
|
||||
- Move focus to first error field on submission failure
|
||||
- Announce errors to screen readers via aria-live
|
||||
</instruction>
|
||||
|
||||
<status-token-mapping>
|
||||
| State | Background | Text | Border |
|
||||
|-------|-----------|------|--------|
|
||||
| Error | bg-destructive | text-destructive-foreground | border-destructive |
|
||||
| Warning | bg-warning | text-warning-foreground | — |
|
||||
| Success | bg-success | text-success-foreground | — |
|
||||
| Disabled | bg-disabled | text-disabled-foreground | — |
|
||||
</status-token-mapping>
|
||||
|
||||
<message-patterns>
|
||||
| Field type | Correct message | Incorrect message |
|
||||
|-----------|----------------|-------------------|
|
||||
| Required | "Report name is required." | "Required" |
|
||||
| Email | "Email must include an @ symbol." | "Invalid" |
|
||||
| Password | "At least 8 characters." | "Too short" |
|
||||
| Number | "Value must be between 1 and 100." | "Invalid" |
|
||||
| Date | "End date must be after start date." | "Invalid date" |
|
||||
|
||||
See `writing-copy.md` for full message patterns.
|
||||
</message-patterns>
|
||||
|
||||
<field-validation-states>
|
||||
Every form field must support the validation states applicable to its type. Use this table to determine which states to implement:
|
||||
|
||||
| Field type | required | format | length | range | uniqueness |
|
||||
|-----------|----------|--------|--------|-------|------------|
|
||||
| Text Input | yes | — | optional | — | optional |
|
||||
| Email Input | yes | yes | — | — | optional |
|
||||
| Password | yes | yes | yes | — | — |
|
||||
| Number Input | yes | — | — | yes | — |
|
||||
| Date Picker | yes | — | — | yes | — |
|
||||
| Textarea | yes | — | yes | — | — |
|
||||
| Select | yes | — | — | — | — |
|
||||
| Combobox | yes | — | — | — | — |
|
||||
| Checkbox | — | — | — | — | — |
|
||||
| File Upload | yes | yes | — | yes (size) | — |
|
||||
|
||||
"yes" = must implement. "optional" = implement if relevant.
|
||||
</field-validation-states>
|
||||
|
||||
<complete-field-example>
|
||||
A complete form field with all states (default, focused, error, success, disabled):
|
||||
|
||||
import { Input } from '@/components/ui/input';
|
||||
import { Label } from '@/components/ui/label';
|
||||
import { HelperText } from '@/components/ui/helper-text';
|
||||
|
||||
{/* Default / Focused state */}
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="report-name">
|
||||
Report name <span className="text-destructive">*</span>
|
||||
</Label>
|
||||
<Input
|
||||
id="report-name"
|
||||
value={name}
|
||||
onChange={(e) => setName(e.target.value)}
|
||||
onBlur={validateName}
|
||||
aria-describedby="report-name-helper"
|
||||
aria-invalid={!!nameError}
|
||||
disabled={isSubmitting}
|
||||
/>
|
||||
{nameError ? (
|
||||
<HelperText id="report-name-helper" variant="error">
|
||||
{nameError}
|
||||
</HelperText>
|
||||
) : (
|
||||
<HelperText id="report-name-helper">
|
||||
A descriptive name for your report.
|
||||
</HelperText>
|
||||
)}
|
||||
</div>
|
||||
|
||||
Key implementation details:
|
||||
- Label with required indicator (asterisk in text-destructive)
|
||||
- Input with aria-describedby linking to HelperText
|
||||
- Input with aria-invalid reflecting error state
|
||||
- Validation on blur via onBlur handler
|
||||
- HelperText swaps between hint (default) and error message
|
||||
- Disabled state during form submission
|
||||
</complete-field-example>
|
||||
</pattern>
|
||||
|
||||
<pattern name="loading-states" priority="critical">
|
||||
<instruction>
|
||||
Any action taking more than 300ms must show a loading indicator using Aura components.
|
||||
</instruction>
|
||||
|
||||
<variants>
|
||||
| Context | Pattern | Aura component |
|
||||
|---------|---------|---------------|
|
||||
| Page load | Skeleton screen | Skeleton |
|
||||
| Button action | Button disabled + spinner | Button loading state |
|
||||
| Data refresh | Overlay spinner | Spinner on existing content |
|
||||
| Long operation | Progress bar + message | Progress |
|
||||
</variants>
|
||||
|
||||
<example type="correct">
|
||||
{/* Button loading during async action */}
|
||||
<Button
|
||||
variant="default"
|
||||
onClick={handleSave}
|
||||
disabled={isSaving}
|
||||
>
|
||||
{isSaving ? (
|
||||
<>
|
||||
<Loader2 className="mr-2 h-4 w-4 animate-spin" />
|
||||
Saving...
|
||||
</>
|
||||
) : (
|
||||
'Save changes'
|
||||
)}
|
||||
</Button>
|
||||
|
||||
{/* Skeleton while page loads */}
|
||||
{isLoading ? (
|
||||
<div className="space-y-4">
|
||||
<Skeleton className="h-8 w-48" />
|
||||
<Skeleton className="h-4 w-full" />
|
||||
<Skeleton className="h-4 w-3/4" />
|
||||
</div>
|
||||
) : (
|
||||
<DataTable data={reports} columns={columns} />
|
||||
)}
|
||||
</example>
|
||||
|
||||
<example type="incorrect">
|
||||
{/* No loading state */}
|
||||
<Button onClick={handleSave}>Save changes</Button>
|
||||
|
||||
{/* Blank screen while loading */}
|
||||
{isLoading ? null : <DataTable data={reports} />}
|
||||
</example>
|
||||
</pattern>
|
||||
|
||||
<pattern name="error-states" priority="critical">
|
||||
<instruction>
|
||||
Every API failure must show a user-facing message using Aura Alert component. Never fail silently.
|
||||
See `writing-copy.md` for message wording.
|
||||
</instruction>
|
||||
|
||||
<full-example>
|
||||
import { Alert, AlertDescription } from '@/components/ui/alert';
|
||||
import { AlertCircle } from 'lucide-react';
|
||||
|
||||
{error && (
|
||||
<Alert variant="destructive">
|
||||
<AlertCircle className="h-4 w-4" />
|
||||
<AlertDescription>
|
||||
{error}
|
||||
</AlertDescription>
|
||||
<Button
|
||||
variant="secondary"
|
||||
size="sm"
|
||||
onClick={retry}
|
||||
className="ml-auto"
|
||||
>
|
||||
Try again
|
||||
</Button>
|
||||
</Alert>
|
||||
)}
|
||||
</full-example>
|
||||
</pattern>
|
||||
|
||||
<pattern name="success-feedback" priority="high">
|
||||
<instruction>
|
||||
Use Sonner toast for brief confirmations.
|
||||
</instruction>
|
||||
|
||||
<example type="correct">
|
||||
import { toast } from 'sonner';
|
||||
|
||||
// After save
|
||||
toast.success('Report saved successfully.');
|
||||
|
||||
// After delete
|
||||
toast.success('Report deleted.');
|
||||
|
||||
// After bulk action
|
||||
toast.success(`${count} items archived.`);
|
||||
</example>
|
||||
|
||||
<example type="incorrect">
|
||||
// No feedback
|
||||
await saveReport(data);
|
||||
navigate('/reports');
|
||||
|
||||
// Vague
|
||||
toast.success('Done!');
|
||||
</example>
|
||||
</pattern>
|
||||
|
||||
<pattern name="confirmation-dialogs" priority="critical">
|
||||
<instruction>
|
||||
Destructive actions must show Dialog with specific action verb. See `writing-copy.md` for copy.
|
||||
</instruction>
|
||||
|
||||
<full-example>
|
||||
import {
|
||||
Dialog, DialogContent, DialogDescription,
|
||||
DialogFooter, DialogHeader, DialogTitle,
|
||||
} from '@/components/ui/dialog';
|
||||
|
||||
<Dialog open={showDelete} onOpenChange={setShowDelete}>
|
||||
<DialogContent>
|
||||
<DialogHeader>
|
||||
<DialogTitle>Delete this report?</DialogTitle>
|
||||
<DialogDescription>
|
||||
This will permanently remove "{report.name}" and
|
||||
all associated data. This cannot be undone.
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
<DialogFooter>
|
||||
<Button
|
||||
variant="secondary"
|
||||
onClick={() => setShowDelete(false)}
|
||||
>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button
|
||||
variant="destructive"
|
||||
onClick={handleDelete}
|
||||
>
|
||||
Delete report
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
</full-example>
|
||||
|
||||
<example type="incorrect">
|
||||
{/* Yes/No, no description, wrong variant */}
|
||||
<Dialog open={show}>
|
||||
<DialogContent>
|
||||
<DialogTitle>Confirm</DialogTitle>
|
||||
<DialogDescription>Are you sure?</DialogDescription>
|
||||
<DialogFooter>
|
||||
<Button variant="secondary">No</Button>
|
||||
<Button variant="default">Yes</Button>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
</example>
|
||||
</pattern>
|
||||
|
||||
</patterns>
|
||||
|
||||
<page-accessibility priority="critical">
|
||||
|
||||
<responsibility name="keyboard-navigation">
|
||||
<instruction>
|
||||
- Tab order follows visual reading order
|
||||
- Every interactive element reachable via Tab
|
||||
- No keyboard traps
|
||||
- Skip-to-content link on pages with complex nav
|
||||
</instruction>
|
||||
|
||||
<example type="correct">
|
||||
<a href="#main-content" className="sr-only focus:not-sr-only
|
||||
focus:absolute focus:top-4 focus:left-4 focus:z-50
|
||||
focus:bg-background focus:px-4 focus:py-2 focus:rounded-md
|
||||
focus:shadow-focus-ring">
|
||||
Skip to main content
|
||||
</a>
|
||||
<Sidebar />
|
||||
<main id="main-content">
|
||||
<h1>Reports</h1>
|
||||
{/* Content in logical tab order */}
|
||||
</main>
|
||||
</example>
|
||||
</responsibility>
|
||||
|
||||
<responsibility name="heading-hierarchy">
|
||||
<instruction>
|
||||
Use heading levels (H1–H6) in strict sequential order.
|
||||
- One H1 per page (the page title)
|
||||
- Never skip levels (H1 directly to H3)
|
||||
- Never use heading tags for visual sizing — use text-*
|
||||
classes from the Typography foundation instead
|
||||
|
||||
Aura applies text-undefined-foreground to headings by default.
|
||||
</instruction>
|
||||
</responsibility>
|
||||
|
||||
<responsibility name="image-alt-text">
|
||||
<instruction>
|
||||
Every image needs alt. Icons in buttons need aria-label.
|
||||
</instruction>
|
||||
|
||||
<examples>
|
||||
| Type | Approach | Example |
|
||||
|------|----------|---------|
|
||||
| Informational | Describe content | alt="Chart: output up 20%" |
|
||||
| Decorative | Empty | alt="" |
|
||||
| Icon button | aria-label on parent | aria-label="Delete report" |
|
||||
| Icon with label | Hide icon | aria-hidden="true" on icon |
|
||||
</examples>
|
||||
|
||||
<example type="correct">
|
||||
{/* Icon + text: hide icon from screen reader */}
|
||||
<Button variant="destructive">
|
||||
<TrashIcon className="h-4 w-4 mr-2" aria-hidden="true" />
|
||||
Delete report
|
||||
</Button>
|
||||
|
||||
{/* Icon only: label on button */}
|
||||
<Button variant="ghost" size="icon" aria-label="Delete report">
|
||||
<TrashIcon className="h-4 w-4" />
|
||||
</Button>
|
||||
</example>
|
||||
</responsibility>
|
||||
|
||||
<responsibility name="dynamic-content">
|
||||
<examples>
|
||||
| Scenario | Method |
|
||||
|----------|--------|
|
||||
| Search results update | aria-live="polite" |
|
||||
| Form error | aria-live="assertive" |
|
||||
| Toast | Sonner handles this |
|
||||
| Dialog opens | Focus moves to dialog (Aura handles) |
|
||||
| Dialog closes | Return focus to trigger |
|
||||
</examples>
|
||||
|
||||
<example type="correct">
|
||||
{/* Screen reader announcement for filtered results */}
|
||||
<div aria-live="polite" className="sr-only">
|
||||
{results.length} results found for "{query}"
|
||||
</div>
|
||||
<DataTable data={results} columns={columns} />
|
||||
</example>
|
||||
</responsibility>
|
||||
|
||||
<responsibility name="color-independence">
|
||||
<instruction>
|
||||
Never use color alone to convey meaning.
|
||||
</instruction>
|
||||
|
||||
<example type="correct">
|
||||
{/* Status with text + color */}
|
||||
<span className="inline-flex items-center gap-1.5
|
||||
bg-success text-success-foreground text-xs px-2.5 py-0.5
|
||||
rounded-full">
|
||||
<CheckCircle className="h-3 w-3" aria-hidden="true" />
|
||||
Active
|
||||
</span>
|
||||
</example>
|
||||
|
||||
<example type="incorrect">
|
||||
{/* Color only — invisible to colorblind users */}
|
||||
<span className="h-2 w-2 rounded-full bg-success" />
|
||||
</example>
|
||||
</responsibility>
|
||||
|
||||
</page-accessibility>
|
||||
|
||||
<translation-and-testing priority="medium">
|
||||
Short sentences and simple grammar translate more reliably. Plan for text expansion in localized UIs (e.g. German often adds 30–40% length); allow flexible button and title widths.
|
||||
|
||||
For automated checks, use WAVE, axe DevTools, or Lighthouse in Chrome DevTools. For manual verification, unplug the mouse and complete primary tasks with keyboard only; spot-check with VoiceOver (Mac) or NVDA (Windows) for critical flows.
|
||||
</translation-and-testing>
|
||||
|
||||
<self-check>
|
||||
Before submitting any page:
|
||||
- [ ] Tab through all elements in logical order?
|
||||
- [ ] Every button/link works with Enter/Space?
|
||||
- [ ] Every dialog opens/closes with keyboard?
|
||||
- [ ] Escape closes dialogs, popovers, dropdowns?
|
||||
- [ ] Every image has appropriate alt text?
|
||||
- [ ] Every form field has a visible label?
|
||||
- [ ] Non-color indicator for every status?
|
||||
- [ ] Headings follow H1 → H2 → H3?
|
||||
- [ ] Dynamic updates announced to screen readers?
|
||||
- [ ] Focus ring (shadow-focus-ring) visible on all elements?
|
||||
</self-check>
|
||||
|
||||
<edge-cases>
|
||||
**Forms and async**
|
||||
1. Destructive action with undo? — Still confirm. Mention undo in body: "You can undo within 30 seconds."
|
||||
2. Bulk delete? — One confirmation: "Delete 12 reports?"
|
||||
3. Auto-save? — Subtle "Saved" indicator, not toast each time.
|
||||
4. Error in multi-step flow? — Don't lose progress. Show error on current step. Let user retry.
|
||||
|
||||
**Accessibility**
|
||||
1. Complex data viz? — Text summary via alt or sr-only text.
|
||||
2. Drag-and-drop? — Keyboard alternative required.
|
||||
3. Real-time dashboard? — aria-live="polite", not "assertive".
|
||||
4. Third-party embed? — iframe with descriptive title.
|
||||
</edge-cases>
|
||||
Reference in New Issue
Block a user